<?php
// +-------------------------------------------------------------------------------
// | DingZone [ A Rapid Development Framework For Dingtalk ]
// +-------------------------------------------------------------------------------
// | 钉钉API帮助类 v1.0
// |
// | Copyright (c) 2014-2018 http://www.heimalanshi.com All rights reserved.
// | Author : Jie
// | Date : 2018-08-09
// +-------------------------------------------------------------------------------

namespace ding;

use app\core\model\EnterpriseModule;
use common\Encryption;
use common\Http;
use common\Random;
use ding\model\EmployeeUserStatusEnum;
use ding\msg\robot\RobotMsg;
use ding\msg\chat\ChatMsg;
use ding\request\CorpExtcontactSaveRequest;
use ding\request\DepartmentSaveRequest;
use ding\request\MicroappSaveRequest;
use ding\request\SmartworkBpmsProcessinstanceCreateRequest;
use ding\request\UserSaveRequest;
use ding\request\WorkrecordAddRequest;
use think\App;
use think\Cache;
use think\Config;
use think\Log;
use think\Request;

/**
 * 钉钉API帮助类
 */
class ApiHelper {
    /**
     * oapi地址
     */
    const OAPI_URL = "https://oapi.dingtalk.com/";
    const OAPI_V2_URL = "https://oapi.dingtalk.com/topapi/";
    /**
     * top接口地址
     */
    const TOP_URL = "https://eco.taobao.com/router/rest";

    /**
     * 所有的api接口列表
     *
     * @var array
     */
    public static $apis = [
        'gettoken' => [
            'title' => '获取全局AccessToken',
            'group' => '权限验证',
            'desc' => 'Access_Token是企业访问钉钉开放平台全局接口的唯一凭证，即调用接口时需携带Access_Token'
        ],
        'get_jsapi_ticket' => [
            'title' => '获取jsapi_ticket',
            'group' => '权限验证',
            'desc' => '企业在使用微应用中的JS API时，需要先从钉钉开放平台接口获取jsapi_ticket生成签名数据，并将最终签名用的部分字段及签名结果返回到H5中，JS API底层将通过这些数据判断H5是否有权限使用JS API。'
        ],
        'sns/gettoken' => [
            'title' => '获取钉钉开放应用的ACCESS_TOKEN',
            'group' => '扫码登录',
            'desc' => '钉钉扫码登录，需要获取该ACCESS_TOKEN'
        ],
        'sns/get_persistent_code' => [
            'title' => '获取用户授权的持久授权码',
            'group' => '扫码登录',
            'desc' => ''
        ],
        'sns/get_sns_token' => [
            'title' => '获取用户授权的SNS_TOKEN',
            'group' => '扫码登录',
            'desc' => ''
        ],
        'sns/getuserinfo' => [
            'title' => '获取用户授权的个人信息',
            'group' => '扫码登录',
            'desc' => '钉钉扫码登录成功后，可以通过此接口获取到用户的基础信息'
        ],
        'user/getuserinfo' => [
            'title' => '免登获取用户信息',
            'group' => '用户',
            'desc' => '应用在钉钉APP内免登场景下，通过jsapi拿到用户对应的code，再通过CODE获取到对应的用户信息'
        ],
        'user/get' => [
            'title' => '获取成员详情',
            'group' => '用户',
            'desc' => '获取企业内员工的姓名、电话、职位等详细信息',
            'privacySupportFields' => [
                "dingId" => "员工钉钉全局ID",
                "unionid" => "员工唯一标识ID",
                "openId" => "在唯一标识关注者身份的id",
                "userid" => "员工企业内唯一ID",

                "jobnumber" => "员工工号",
                "name" => "员工名称",
                "avatar" => "员工头像",
                "remark" => "员工备注",

                "tel" => "员工分机号",
                "mobile" => "员工手机号码",
                "stateCode" => "员工手机号区号",
                "isHide" => "员工号码是否隐藏",
                "email" => "员工的电子邮箱",
                "orgEmail" => "员工的企业邮箱",

                "workPlace" => "员工办公地点",
                "hiredDate" => "员工入职时间",

                "isLeaderInDepts" => "员工是否为部门主管",
                "isBoss" => "员工是否为企业老板",
                "isSenior" => "员工是否高管",
                "active" => "员工是否激活",

                "department" => "员工所在部门列表",
                "orderInDepts" => "员工在部门中的排序",
                "position" => "员工职位",
                "roles" => "员工角色信息",

                "extattr" => "员工拓展属性",

                "isAdmin" => "是否为管理员"
            ]
        ],
        'user/create' => [
            'title' => '创建企业内部成员',
            'group' => '用户',
            'desc' => '创建企业内部成员用户'
        ],
        'user/update' => [
            'title' => '更新企业内部成员信息',
            'group' => '用户',
            'desc' => '更新企业内部成员用户信息'
        ],
        'user/delete' => [
            'title' => '删除企业内部成员',
            'group' => '用户',
            'desc' => ''
        ],
        'user/batchdelete' => [
            'title' => '批量删除企业内部成员',
            'group' => '用户',
            'desc' => ''
        ],
        'user/simplelist' => [
            'title' => '获取部门成员列表',
            'group' => '用户',
            'desc' => '获取部门成员列表，返回内容包括用户名、用户id',
            'privacySupportFields' => [
                "userlist.userid" => "员工企业内唯一ID",
                "userlist.name" => "员工名称"
            ]
        ],
        'user/list' => [
            'title' => '获取部门成员详情列表',
            'group' => '用户',
            'desc' => '获取部门成员的详情列表，返回内容包括用户名、用户id、电话号码、职位等等',
            'privacySupportFields' => [
                "userlist.dingId" => "员工钉钉全局ID",
                "userlist.unionid" => "员工唯一标识ID",
                "userlist.openId" => "在唯一标识关注者身份的id",
                "userlist.userid" => "员工企业内唯一ID",

                "userlist.jobnumber" => "员工工号",
                "userlist.name" => "员工名称",
                "userlist.avatar" => "员工头像",
                "userlist.remark" => "员工备注",

                "userlist.tel" => "员工分机号",
                "userlist.mobile" => "员工手机号码",
                "userlist.stateCode" => "员工手机号区号",
                "userlist.isHide" => "员工号码是否隐藏",
                "userlist.email" => "员工的电子邮箱",
                "userlist.orgEmail" => "员工的企业邮箱",

                "userlist.workPlace" => "员工办公地点",
                "userlist.hiredDate" => "员工入职时间",

                "userlist.isLeaderInDepts" => "员工是否为部门主管",
                "userlist.isBoss" => "员工是否为企业老板",
                "userlist.isSenior" => "员工是否高管",
                "userlist.active" => "员工是否激活",

                "userlist.department" => "员工所在部门列表",
                "userlist.orderInDepts" => "员工在部门中的排序",
                "userlist.position" => "员工职位",
                "userlist.roles" => "员工角色信息",

                "userlist.extattr" => "员工拓展属性",
            ]
        ],
        'user/getUseridByUnionid' => [
            'title' => '根据unionid获取成员的userid',
            'group' => '用户',
            'desc' => '根据unionid获取成员的userid。扫描登录时，需要用此接口获取用户信息'
        ],
        'user/get_org_user_count' => [
            'title' => '获取企业员工总数',
            'group' => '用户',
            'desc' => ''
        ],
        'user/get_admin' => [
            'title' => '获取管理员列表',
            'group' => '管理员',
            'desc' => ''
        ],
        'user/can_access_microapp' => [
            'title' => '获取管理员的微应用管理权限',
            'group' => '管理员',
            'desc' => ''
        ],
        'department/list_ids' => [
            'title' => '获取部门id列表',
            'group' => '通讯录',
            'desc' => ''
        ],
        'department/list' => [
            'title' => '获取部门列表',
            'group' => '通讯录',
            'desc' => ''
        ],
        'department/get' => [
            'title' => '获取部门详情',
            'group' => '通讯录',
            'desc' => ''
        ],
        'department/create' => [
            'title' => '创建部门',
            'group' => '通讯录',
            'desc' => ''
        ],
        'department/update' => [
            'title' => '更新部门信息',
            'group' => '通讯录',
            'desc' => ''
        ],
        'department/delete' => [
            'title' => '删除部门',
            'group' => '通讯录',
            'desc' => ''
        ],
        'department/list_parent_depts_by_dept' => [
            'title' => '查询部门的所有上级父部门路径',
            'group' => '通讯录',
            'desc' => ''
        ],
        'department/list_parent_depts' => [
            'title' => '查询指定用户的所有上级父部门路径',
            'group' => '通讯录',
            'desc' => ''
        ],
        'dingtalk.corp.emp.search' => [
            'title' => '企业通讯录员工搜索',
            'group' => '通讯录',
            'desc' => '钉钉企业通讯录搜索接口, 支持翻页',
            'privacySupportFields' => [
                'list.name' => '员工名称',
                'list.title' => '员工职位',
                'list.nick' => '员工昵称',
                'list.email' => '员工邮箱',
                'list.mobile' => '员工手机号码',
                'list.hasSubordinate' => '是否有下级',
                'list.extFields' => '员工拓展信息',
                'list.depts' => '员工部门信息'
            ]
        ],
        'robot/send' => [
            'title' => '自定义机器人消息发送',
            'group' => '机器人',
            'desc' => '向自定义机器人发送消息'
        ],
        'chat/create' => [
            'title' => '创建群',
            'group' => '群',
            'desc' => '新建群聊天'
        ],
        'chat/update' => [
            'title' => '修改群信息',
            'group' => '群',
            'desc' => '修改群信息，例如群名称、群主、人员信息'
        ],
        'chat/get' => [
            'title' => '获取群信息',
            'group' => '群',
            'desc' => '获取群信息，例如群名称、群主、人员信息'
        ],
        'chat/send' => [
            'title' => '发送群消息',
            'group' => '消息',
            'desc' => '获取群信息，例如群名称、群主、人员信息'
        ],
        'chat/getReadList' => [
            'title' => '查询群消息已读人员列表',
            'group' => '消息',
            'desc' => '分页查询发送到群中的消息被哪些人员阅读了'
        ],
        'message/send_to_conversation' => [
            'title' => '发送普通消息',
            'group' => '消息',
            'desc' => '用户在页面上唤起联系人界面选择之后，给个人或者群发送消息'
        ],
        'dingtalk.corp.message.corpconversation.asyncsend' => [
            'title' => '企业会话消息异步发送',
            'group' => '消息',
            'desc' => ''
        ],
        'dingtalk.corp.message.corpconversation.asyncsendbycode' => [
            'title' => '通过用户授权码异步向企业会话发送消息',
            'group' => '消息',
            'desc' => ''
        ],
        'dingtalk.corp.message.corpconversation.getsendprogress' => [
            'title' => '获取异步发送企业会话消息的进度',
            'group' => '消息',
            'desc' => ''
        ],
        'dingtalk.corp.message.corpconversation.getsendresult' => [
            'title' => '获取异步向企业会话发送消息的结果',
            'group' => '消息',
            'desc' => ''
        ],
        'call_back/register_call_back' => [
            'title' => '注册通讯录事件回调接口',
            'group' => '通讯录',
            'desc' => ''
        ],
        'call_back/get_call_back' => [
            'title' => '查询事件回调接口',
            'group' => '通讯录',
            'desc' => ''
        ],
        'call_back/update_call_back' => [
            'title' => '更新事件回调接口',
            'group' => '通讯录',
            'desc' => ''
        ],
        'call_back/delete_call_back' => [
            'title' => '删除事件回调接口',
            'group' => '通讯录',
            'desc' => ''
        ],
        'call_back/get_call_back_failed_result' => [
            'title' => '获取回调失败的结果',
            'group' => '通讯录',
            'desc' => ''
        ],
        'sso/gettoken' => [
            'title' => '获取微应用后台免登的SsoToken',
            'group' => '后台免登',
            'desc' => '管理后台免登所需的token，微应用后台管理员免登可以实现让企业管理员在钉钉企业管理后台直接进入到微应用的管理后台（该管理后台与钉钉企业管理后台不在同一系统）而不需要登录。'
        ],
        'sso/getuserinfo' => [
            'title' => '获取微应用管理员的身份信息',
            'group' => '后台免登',
            'desc' => '用于微应用后台管理员免登中用来换取管理员用户信息'
        ],
        'microapp/create' => [
            'title' => '创建微应用',
            'group' => '微应用',
            'desc' => ''
        ],
        'microapp/update' => [
            'title' => '更新微应用',
            'group' => '微应用',
            'desc' => ''
        ],
        'microapp/delete' => [
            'title' => '删除微应用',
            'group' => '微应用',
            'desc' => ''
        ],
        'microapp/list' => [
            'title' => '微应用列表',
            'group' => '微应用',
            'desc' => ''
        ],
        'microapp/list_by_userid' => [
            'title' => '获取用户可见的微应用列表',
            'group' => '微应用',
            'desc' => ''
        ],
        'microapp/visible_scopes' => [
            'title' => '获取微应用的可见范围',
            'group' => '微应用',
            'desc' => ''
        ],
        'microapp/set_visible_scopes' => [
            'title' => '设置微应用的可见范围',
            'group' => '微应用',
            'desc' => ''
        ],
        'media/upload' => [
            'title' => '上传媒体文件',
            'group' => '文件',
            'desc' => '用于上传图片、语音等媒体资源文件以及普通文件（如doc，ppt）'
        ],
        'media/downloadFile' => [
            'title' => '获取媒体文件',
            'group' => '文件',
            'desc' => ''
        ],
        'cspace/add_to_single_chat' => [
            'title' => '发送文件给指定用户',
            'group' => '钉盘',
            'desc' => '将文件发送给指定用户，用户将收到以微应用名义发送的一条文件消息。'
        ],
        'cspace/add' => [
            'title' => '新增文件到用户钉盘',
            'group' => '钉盘',
            'desc' => '将上传的文件放到指定用户的钉盘里。'
        ],
        'cspace/get_custom_space' => [
            'title' => '获取企业下的自定义空间',
            'group' => '钉盘',
            'desc' => ''
        ],
        'cspace/grant_custom_space' => [
            'title' => '授权用户访问企业下的自定义空间',
            'group' => '钉盘',
            'desc' => ''
        ],
        'file/upload/transaction' => [
            'title' => '开始或者完成文件分块上传',
            'group' => '钉盘',
            'desc' => ''
        ],
        'file/upload/chunk' => [
            'title' => '分块上传文件块',
            'group' => '钉盘',
            'desc' => ''
        ],
        'file/upload/single' => [
            'title' => '单步文件上传',
            'group' => '钉盘',
            'desc' => ''
        ],
        'dingtalk.corp.role.simplelist' => [
            'title' => '获取角色的员工列表',
            'group' => '角色',
            'desc' => ''
        ],
        'dingtalk.corp.role.list' => [
            'title' => '获取企业角色列表',
            'group' => '角色',
            'desc' => ''
        ],
        'dingtalk.corp.role.addrolesforemps' => [
            'title' => '批量为员工增加角色信息',
            'group' => '角色',
            'desc' => '企业在做企业员工管理的时候，需要对部分员工进行角色分类，该接口可以批量为员工增加角色信息'
        ],
        'dingtalk.corp.role.removerolesforemps' => [
            'title' => '批量删除员工角的色信息',
            'group' => '角色',
            'desc' => '企业在做企业员工管理的时候，需要对部分员工进行角色分类，该接口可以批量删除员工的角色信息。 角色在钉钉的组织结构里面就是标签的意思，你可以批量为一批用户添加一批角色信息'
        ],
        'dingtalk.corp.role.deleterole' => [
            'title' => '删除角色信息',
            'group' => '角色',
            'desc' => '企业在做企业内部组织结构角色管理的时候，如果需要删除该企业下某个角色信息，可以调用该接口。调用的前提是该角色下面的所有员工都已经被删除该角色'
        ],
        'dingtalk.corp.role.getrolegroup' => [
            'title' => '获取角色组信息',
            'group' => '角色',
            'desc' => '该接口通过groupId参数可以获取该角色组详细信息以及下面所有关联的角色的信息'
        ],
        'dingtalk.corp.health.stepinfo.getuserstatus' => [
            'title' => '查询用户是否开启了钉钉运动',
            'group' => '钉钉运行',
            'desc' => ''
        ],
        'dingtalk.corp.health.stepinfo.listbyuserid' => [
            'title' => '批量查询多个用户的钉钉运动步数',
            'group' => '钉钉运行',
            'desc' => ''
        ],
        'dingtalk.corp.extcontact.create' => [
            'title' => '添加企业外部联系人',
            'group' => '外部联系人',
            'desc' => ''
        ],
        'dingtalk.corp.extcontact.update' => [
            'title' => '更新企业外部联系人',
            'group' => '外部联系人',
            'desc' => ''
        ],
        'dingtalk.corp.extcontact.list' => [
            'title' => '获取企业外部联系人列表',
            'group' => '外部联系人',
            'desc' => ''
        ],
        'dingtalk.corp.extcontact.get' => [
            'title' => '获取企业外部联系人详情',
            'group' => '外部联系人',
            'desc' => ''
        ],
        'dingtalk.corp.extcontact.listlabelgroups' => [
            'title' => '获取企业外部联系人标签列表',
            'group' => '外部联系人',
            'desc' => ''
        ],
        'dingtalk.smartwork.attends.listschedule' => [
            'title' => '按天查询企业考勤排班全量信息',
            'group' => '考勤',
            'desc' => ''
        ],
        'dingtalk.smartwork.attends.getsimplegroups' => [
            'title' => '获取公司自身的考勤组列表详情信息',
            'group' => '考勤',
            'desc' => ''
        ],
        'attendance/listRecord' => [
            'title' => '获取考勤打卡记录',
            'group' => '考勤',
            'desc' => ''
        ],
        'attendance/list' => [
            'title' => '获取考勤打卡结果',
            'group' => '考勤',
            'desc' => ''
        ],
        'dingtalk.smartwork.attends.getleaveapproveduration' => [
            'title' => '计算请假时长',
            'group' => '考勤',
            'desc' => ''
        ],
        'dingtalk.smartwork.attends.getusergroup' => [
            'title' => '获取用户考勤组',
            'group' => '考勤',
            'desc' => ''
        ],
        'checkin/record' => [
            'title' => '获取部门签到记录',
            'group' => '签到',
            'desc' => ''
        ],
        'dingtalk.smartwork.checkin.record.get' => [
            'title' => '查询多个用户一段时间范围内的签到记录',
            'group' => '签到',
            'desc' => ''
        ],
        'dingtalk.smartwork.bpms.process.copy' => [
            'title' => '复制审批流',
            'group' => '审批',
            'desc' => ''
        ],
        'dingtalk.smartwork.bpms.process.sync' => [
            'title' => '更新审批流',
            'group' => '审批',
            'desc' => ''
        ],
        'dingtalk.smartwork.bpms.processinstance.create' => [
            'title' => '发起审批实例',
            'group' => '审批',
            'desc' => ''
        ],
        'dingtalk.smartwork.bpms.processinstance.list' => [
            'title' => '获取审批实例列表',
            'group' => '审批',
            'desc' => ''
        ],
        'dingtalk.smartwork.bpms.processinstance.get' => [
            'title' => '获取单个审批实例详情',
            'group' => '审批',
            'desc' => ''
        ],
        'dingtalk.corp.report.list' => [
            'title' => '查询企业员工发出的日志列表',
            'group' => '日志',
            'desc' => ''
        ],
        'dingtalk.smartwork.blackboard.listtopten' => [
            'title' => '列出用户有权限看到的公告列表',
            'group' => '公告',
            'desc' => ''
        ],
        'workrecord/add' => [
            'title' => '发起待办',
            'group' => '待办',
            'desc' => '调用该接口发起一个待办事项，该待办事项会出现在钉钉客户端“待办事项”页面，与钉钉审批待办事项并列。',
            '__api_prefix_url' => self::OAPI_V2_URL
        ],
        'workrecord/update' => [
            'title' => '更新待办',
            'group' => '待办',
            'desc' => '调用成功后，该待办事项会该用户的“待办事项”列表页面消失',
            '__api_prefix_url' => self::OAPI_V2_URL
        ],
        'workrecord/getbyuserid' => [
            'title' => '获取用户待办事项',
            'group' => '待办',
            'desc' => '分页获取用户的待办事项列表',
            '__api_prefix_url' => self::OAPI_V2_URL
        ],
        'smartwork/hrm/employee/list' => [
            'title' => '获取员工花名册字段信息',
            'group' => '智能人事',
            'desc' => '根据员工userid，批量访问员工花名册字段信息。',
            '__api_prefix_url' => self::OAPI_V2_URL
        ],
        'smartwork/hrm/employee/querydimission' => [
            'title' => '查询企业离职员工列表',
            'group' => '智能人事',
            'desc' => '分页查询企业离职员工userid列表',
            '__api_prefix_url' => self::OAPI_V2_URL
        ],
        'smartwork/hrm/employee/querypreentry' => [
            'title' => '查询企业待入职员工列表',
            'group' => '智能人事',
            'desc' => '分页查询企业离职员工userid列表',
            '__api_prefix_url' => self::OAPI_V2_URL
        ],
        'smartwork/hrm/employee/queryonjob' => [
            'title' => '查询企业在职员工列表',
            'group' => '智能人事',
            'desc' => '分页查询企业在职员工userid列表',
            '__api_prefix_url' => self::OAPI_V2_URL
        ],
        'smartwork/hrm/employee/listdimission' => [
            'title' => '获取离职员工离职信息',
            'group' => '智能人事',
            'desc' => '根据员工userid列表，批量查询员工的离职信息。传入非离职员工userid，不会返回信息',
            '__api_prefix_url' => self::OAPI_V2_URL
        ]
    ];

    /**
     * 企业Id
     *
     * @var string
     */
    private $corpid;

    /**
     * 企业应用的凭证密钥
     *
     * @var string
     */
    private $corpsecret;

    /**
     * 微应用id
     *
     * @var
     */
    private $agentId;
    /**
     * 扫码用appid
     *
     * @var
     */
    private $appid;
    /**
     * 扫码用app密匙
     *
     * @var
     */
    private $appsecret;

    /**
     * 企业id
     *
     * @var
     */
    private $eid;

    /**
     * 企业信息
     *
     * @var
     */
    private $enterprise;

    /**
     * 模块id
     *
     * @var
     */
    private $mid;

    /**
     * 缓存key前缀
     *
     * @var string
     */
    private $cacheKeyPreFix;

    /**
     * access_token
     *
     * @var
     */
    private $token;

    /**
     * api权限校验器
     *
     * @var ApiAuthHelper
     */
    private $apiAuthHelper;

    /**
     * 最近一次api调用结果
     * @var
     */
    private $lastStatus;

    /**
     * 构造函数
     *
     * @param $enterprise
     * @param $module
     */
    public function __construct($enterprise, $module) {
        $this->enterprise = $enterprise;
        $this->eid = $enterprise['id'];
        $key = DEFAULT_DECRYPT_KEY;
        $this->corpid = Encryption::decrypt($enterprise['corpid'], $key);
        $this->corpsecret = Encryption::decrypt($enterprise['corpsecret'], $key);
        $this->appid = Encryption::decrypt($enterprise['appid'], $key);
        $this->appsecret = Encryption::decrypt($enterprise['appsecret'], $key);
        $this->cacheKeyPreFix = "core.api." . md5('ding_' . $this->eid . '_' . $this->corpid . "_" . $this->corpsecret) . "_";

        // 创建api权限校验器
        if ($module == null) {
            if (!\C::CORE) {
                die("参数错误");
            } else {
                $this->mid = -1;
                $this->apiAuthHelper = null;
            }
        } else {
            $this->mid = $module['id'];
            $this->apiAuthHelper = new ApiAuthHelper($this->eid, $this->mid);
        }
    }

    /**
     * 获取corpid
     *
     * @return string
     */
    public function getCorpid() {
        return $this->corpid;
    }

    /**
     * 获取appid
     *
     * @return string
     */
    public function getAppid() {
        return $this->appid;
    }

    /**
     * 获取微应用id
     *
     * @return string
     * @throws \think\exception\DbException
     */
    public function getAgentId() {
        if (empty($this->agentId)) {
            $enterpriseModule = EnterpriseModule::getById($this->eid, $this->mid);
            if (!empty($enterpriseModule)) {
                $this->agentId = $enterpriseModule['agent_id'];
            }
        }
        if (empty($this->agentId)) {
            $this->agentId = $this->enterprise['agent_id'];
        }
        return $this->agentId;
    }

    /**
     * 获取所有的api列表
     *
     * @return array
     */
    public function getApis() {
        return $this::$apis;
    }

    /**
     * 获取最近一次失败错误日志
     *
     * @return mixed
     */
    public function getLastStatusMsg() {
        return is_array($this->lastStatus) ? $this->lastStatus['errmsg'] : null;
    }

    /**
     * 获取最近一次的api调用信息
     *
     * @return mixed
     */
    public function getLastStatus() {
        return $this->lastStatus;
    }

    /**
     * 获取最近一次的api错误信息
     *
     * @return mixed
     */
    public function getLastError() {
        return $this->lastStatus && isset($this->lastStatus['errcode']) && $this->lastStatus['errcode'] != 0 ?
            $this->lastStatus : null;
    }

    /**
     * 判断响应内容是否为错误
     *
     * @param $response
     * @return bool
     */
    public function isError($response) {
        if (!$response) {
            return true;
        }
        if (isset($response['errcode']) && $response['errcode'] != 0) {
            return true;
        }
        return false;
    }

    /**
     * 输出debug日志
     *
     * @param string|array $logs
     */
    private function _debug($logs) {
        if (!App::$debug) {
            return;
        }
        $this->_info($logs);
    }

    /**
     * 输出日志
     *
     * @param string|array $logs
     */
    private function _info($logs) {
        if (is_array($logs)) {
            $str = "";
            foreach ($logs as $l) {
                if (is_array($l) || is_object($l)) {
                    $str .= json_encode($l);
                } else {
                    $str .= $l;
                }
            }
        } else {
            $str = $logs;
        }
        $str = Request::instance()->url() . " ||| " . $str;
        Log::record($str, "ding");
    }

    /**
     * 从结果中提取内容
     *
     * @param array $response
     * @param string $key
     * @param $defaultVal
     * @return bool|mixed
     */
    private function _fetch($response, $key, $defaultVal = null) {
        if (!is_array($response) || !isset($response[$key])) {
            return $defaultVal;
        }
        return $response[$key];
    }

    /**
     * 验证api是否可以访问
     *
     * @param $apiName
     * @return bool
     * @throws \think\exception\DbException
     */
    private function _authApi($apiName) {
        if (!isset(self::$apis[$apiName])) {
            $msg = ["api auth failed. ", $apiName, " is not in api list. eid = ", $this->eid, ' mid = ', $this->mid];
            $this->_info($msg);
            if (App::$debug) {
                die(join("", $msg));
            }
            return false;
        }
        if ($this->apiAuthHelper == null) {
            if (!\C::CORE) {
                return false;
            } else {
                return true;
            }
        }
        if (!$this->apiAuthHelper->auth($apiName)) {
            $msg = ["api auth failed. ", $apiName, " is not allowed to call. eid = ", $this->eid, ' mid = ', $this->mid];
            $this->_info($msg);
            if (App::$debug) {
                die(join("", $msg));
            }
            return false;
        }
        return true;
    }


    /**
     * 处理返回结果
     *
     * @param $apiName
     * @param $responseContent
     * @return array|mixed
     * @throws \think\exception\DbException
     */
    private function _handleResponse($apiName, $responseContent) {
        if (is_string($responseContent)) {
            $response = json_decode($responseContent, true);
        } else {
            $response = $responseContent;
        }
        if (!$response) {
            $response = [
                'errcode' => 99999,
                'errmsg' => $responseContent
            ];
        }

        $this->lastStatus = array(
            'errcode' => $this->_fetch($response, 'errcode', 0),
            'errmsg' => $this->_fetch($response, 'errmsg', 'success')
        );

        if ($this->isError($response)) {
            $this->_info("api " . $apiName . " error. response = " . json_encode($responseContent));
        }

        // 处理隐私字段
        if ($this->apiAuthHelper == null) {
            if (!\C::CORE) {
                $this->lastStatus = array(
                    'errcode' => 99999,
                    'errmsg' => "无权限"
                );
                return false;
            } else {
                return $response;
            }
        }
        return $this->apiAuthHelper->handlePrivacy($apiName, $response);
    }

    /**
     * 调用top api接口
     *
     * @param $apiName
     * @param $data
     * @return array|mixed
     * @throws \think\exception\DbException
     */
    public function callTop($apiName, $data) {
        //验证api
        if (!$this->_authApi($apiName)) {
            $this->lastStatus = [
                'errcode' => 777777,
                'errmsg' => 'api ' . $apiName . ' auth faied.'
            ];
            return $this->lastStatus;
        }

        $url = $this::TOP_URL . "?method=" . urlencode($apiName);
        $url .= "&timestamp=" . urlencode(date("Y-m-d H:i:s"));
        $url .= "&format=json";
        $url .= "&v=2.0";
        $url .= "&simplify=true";
        $url .= "&session=" . $this->gettoken();

        //提交请求
        $response = Http::postForm($url, $data);
        $this->_debug(['call top api url = ', $url, ' data = ', $data, ' response = ', $response]);

        //处理结果
        $response = json_decode($response, true);
        if (isset($response['error_response'])) {
            //出现错误
            $code = $this->_fetch($response['error_response'], 'code', 99999);
            $msg = $this->_fetch($response['error_response'], "msg", "unknown");
            $subCode = $this->_fetch($response['error_response'], 'sub_code', 0);
            $subMsg = $this->_fetch($response['error_response'], 'sub_msg', "");
            $response['errcode'] = $code . "." . $subCode;
            $response['errmsg'] = $msg . "." . $subMsg;
        } else if (isset($response['result']) && (
                $this->_fetch($response['result'], 'is_success', null) === false ||
                $this->_fetch($response['result'], 'success', null) === false
            )
        ) {
            $response['errcode'] = $this->_fetch($response['result'], 'ding_open_errcode', 99999);
            $response['errmsg'] = $this->_fetch($response['result'], 'error_msg', 'unknown');
        } else {
            $response = $this->_fetch($response, 'result');
        }

        return $this->_handleResponse($apiName, $response);
    }

    /**
     * 调用api
     *
     * @param string $apiName
     * @param array $args
     * @param array $data
     * @param bool $post
     * @param bool $postAsForm
     * @return array
     * @throws \think\exception\DbException
     */
    public function call($apiName, $args = [], $data = [], $post = false, $postAsForm = false) {
        //验证api
        if (!$this->_authApi($apiName)) {
            $this->lastStatus = [
                'errcode' => 777777,
                'errmsg' => 'api ' . $apiName . ' auth faied.'
            ];
            return $this->lastStatus;
        }

        $apiInfo = self::$apis[$apiName];
        $urlPrefix = fetch($apiInfo, "__api_prefix_url", self::OAPI_URL);

        //组装url
        $url = $urlPrefix . $apiName . "?_t=" . time();
        if (is_array($args)) {
            foreach ($args as $name => $value) {
                if (is_bool($value)) {
                    $value = $value ? 'true' : 'false';
                }
                $url .= "&" . $name . "=" . $value;
            }
        }

        if (!$post) {
            //get请求
            if (is_array($data)) {
                foreach ($data as $name => $value) {
                    $url .= "&" . $name . "=" . $value;
                }
            }
            $responseContent = Http::get($url, false, false);
            $this->_debug(['get call api url = ', $url, ' response = ', $responseContent]);
        } else {
            //post请求
            if ($postAsForm) {
                //以form方式提交
                $responseContent = Http::postForm($url, $data, false, false);
            } else {
                //以json方式提交
                $responseContent = Http::postJson($url, json_encode($data), false, false);
            }
            $this->_debug(['post call api url = ', $url, ' data = ', $data, ' response = ', $responseContent]);
        }

        // 处理返回结果
        $response = $this->_handleResponse($apiName, $responseContent);

        return $response;
    }

    /**
     * 调用api,自动附带corpid和corpsecret
     *
     * @param string $apiName
     * @param array $data
     * @param bool $post
     * @return array
     * @throws \think\exception\DbException
     */
    public function callWithCorp($apiName, $data = [], $post = false) {
        $args = [
            'corpid' => $this->corpid,
            'corpsecret' => $this->corpsecret
        ];
        return $this->call($apiName, $args, $data, $post);
    }

    /**
     * 调用api，自动附带token
     *
     * @param string $apiName
     * @param array $data
     * @param bool $post
     * @return array
     * @throws \think\exception\DbException
     */
    public function callWithToken($apiName, $data = [], $post = false) {
        $args = ['access_token' => $this->gettoken()];
        return $this->call($apiName, $args, $data, $post);
    }


    /**
     * 到缓存中获取
     *
     * @param string $key
     * @param int $expire
     * @return bool|mixed
     */
    private function getCache($key, $expire) {
        // 先到缓存中获取
        $cacheObj = Cache::get($this->cacheKeyPreFix . $key);
        if (empty($cacheObj)) {
            return false;
        }
        if (isset($expire)) {
            //判断一下缓存中的数据是否过期
            if (isset($cacheObj['data']) && isset($cacheObj['request_time']) && time() - $cacheObj['request_time'] < $expire) {
                // 缓存没过期
                return $cacheObj['data'];
            } else {
                return false;
            }
        }

        return $cacheObj['data'];
    }

    /**
     * 将数据塞入缓存
     *
     * @param string $key
     * @param mixed $data
     * @param int $expire
     */
    private function putCache($key, $data, $expire) {
        $cacheObj = [
            'request_time' => time(),
            'data' => $data
        ];
        Cache::set($this->cacheKeyPreFix . $key, $cacheObj, $expire);
    }


    /**
     * 获取access token
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.CJnzLl&treeId=385&articleId=104980&docType=1#s2
     *
     * @return string
     * @throws \think\exception\DbException
     */
    private function gettoken() {
        if ($this->token) {
            return $this->token;
        }

        // 先到缓存中获取
        $response = $this->getCache("token", 7000);
        if (!empty($response) && !empty($response['access_token'])) {
            return $response['access_token'];
        }

        // 调用接口获取
        $response = $this->callWithCorp("gettoken");
        if (!empty($response) && !empty($response['access_token'])) {
            $this->putCache('token', $response, 7000);
        }
        $this->token = $this->_fetch($response, 'access_token');
        return $this->token;
    }

    /**
     * 获取jsapi_ticket
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.EKwIj0&treeId=385&articleId=104966&docType=1#s0
     *
     * @return string
     * @throws \think\exception\DbException
     */
    public function getJsapiTicket() {
        // 先到缓存中获取
        $response = $this->getCache("jsapiTicket", 7000);
        if (!empty($response) && !empty($response['ticket'])) {
            return $response['ticket'];
        }

        // 调用接口获取
        $response = $this->callWithToken("get_jsapi_ticket");
        if (!empty($response) && !empty($response['ticket'])) {
            $this->putCache('ticket', $response, 7000);
        }
        return $this->_fetch($response, 'ticket');
    }

    /**
     * 获取jsapi的config
     *
     * @param string $url
     * @return array
     * {
     *      "url":"",           //页面地址
     *      "nonceStr":"",      //随机字符串
     *      "agentId":"",       //微应用id
     *      "timeStamp":"",     //当前时间戳
     *      "corpId":"",        //corpid
     *      "signature":""      //生成的签名
     * }
     * @throws \think\exception\DbException
     */
    public function getJsapiConfig($url) {
        $agentId = $this->getAgentId();
        $nonceStr = Random::get();
        $timeStamp = time();
        $url = urldecode($url);
        $jsapiTicket = $this->getJsapiTicket();

        $signature = sha1(
            'jsapi_ticket=' . $jsapiTicket . '&noncestr=' . $nonceStr . '&timestamp=' . $timeStamp . '&url=' . $url
        );

        $config = array(
            'url' => $url,
            'nonceStr' => $nonceStr,
            'agentId' => $agentId,
            'timeStamp' => $timeStamp,
            'corpId' => $this->corpid,
            'signature' => $signature
        );
        $this->_debug(['jsapi config, result = ', $config]);
        return $config;
    }

    /**
     * 获取钉钉开放应用的ACCESS_TOKEN
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.iXxtz8&treeId=385&articleId=104968&docType=1#s0
     *
     * @return string
     * @throws \think\exception\DbException
     */
    public function snsGettoken() {
        // 先到缓存中获取
        $cacheKey = "sns_gettoken_" . $this->appid . "_" . $this->appsecret;
        $response = $this->getCache($cacheKey, 7000);
        if (!empty($response) && !empty($response['access_token'])) {
            return $response['access_token'];
        }

        // 调用接口获取
        $response = $this->call("sns/gettoken", [
            'appid' => $this->appid,
            'appsecret' => $this->appsecret
        ]);
        if (!empty($response) && !empty($response['access_token'])) {
            $this->putCache($cacheKey, $response, 7000);
        }
        return $this->_fetch($response, 'access_token');
    }

    /**
     * 获取用户授权的持久授权码
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.iXxtz8&treeId=385&articleId=104968&docType=1#s1
     *
     * @param $tmpAuthCode
     * @return bool|array
     *      {
     *           "openid": "liSii8KCxxxxx",                                     //用户在当前开放应用内的唯一标识
     *           "persistent_code": "dsa-d-asdasdadHIBIinoninINIn-ssdasd",      //用户给开放应用授权的持久授权码，此码目前无过期时间
     *           "unionid": "7Huu46kk"                                          //用户在当前钉钉开放平台账号范围内的唯一标识，同一个钉钉开放平台账号可以包含多个开放应用，同时也包含ISV的套件应用及企业应用
     *      }
     * @throws \think\exception\DbException
     */
    public function snsGetPersistentCode($tmpAuthCode) {
        $snsToken = $this->snsGettoken();
        if (!$snsToken) {
            return false;
        }
        $response = $this->call("sns/get_persistent_code", [
            'access_token' => $snsToken
        ], [
            'tmp_auth_code' => $tmpAuthCode
        ], true);

        return $response;
    }

    /**
     * 获取用户授权的SNS_TOKEN
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.iXxtz8&treeId=385&articleId=104968&docType=1#s2
     *
     * @param $openid
     * @param $persistentCode
     * @return string
     * @throws \think\exception\DbException
     */
    public function snsGetSnsToken($openid, $persistentCode) {
        // 先到缓存中获取
        $cacheKey = "sns_get_sns_token_" . $openid . "_" . $persistentCode;
        $response = $this->getCache($cacheKey, 7000);
        if (!empty($response) && !empty($response['sns_token'])) {
            return $response['sns_token'];
        }
        $snsToken = $this->snsGettoken();
        if (!$snsToken) {
            return false;
        }

        // 调用接口获取
        $response = $this->call("sns/get_sns_token", [
            'access_token' => $snsToken
        ], [
            'openid' => $openid,
            'persistent_code' => $persistentCode
        ], true);

        if (!empty($response) && !empty($response['sns_token'])) {
            $this->putCache($cacheKey, $response, 7000);
        }
        return $this->_fetch($response, 'sns_token');
    }

    /**
     * 获取用户授权的个人信息
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.iXxtz8&treeId=385&articleId=104968&docType=1#s3
     * {
     *      "nick": "张三",                       //用户在钉钉上面的昵称
     *      "openid": "liSii8KCxxxxx",           //用户在当前开放应用内的唯一标识
     *      "unionid": "7Huu46kk",               //用户在当前开放应用所属的钉钉开放平台账号内的唯一标识
     *      "dingId": "dingId"                   //钉钉Id
     * }
     * @param $snsToken
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function snsGetuserinfo($snsToken) {
        $response = $this->call("sns/getuserinfo", ['sns_token' => $snsToken]);
        return $this->_fetch($response, 'user_info');
    }

    /**
     * 获取部门id列表
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.CJnzLl&treeId=385&articleId=106817&docType=1#s0
     *
     * @param int $parentDepartmentId
     * @return mixed
     * @throws \think\exception\DbException
     */
    public function departmentListIds($parentDepartmentId = 1) {
        $response = $this->callWithToken("department/list_ids", ['id' => $parentDepartmentId]);
        return $this->_fetch($response, 'sub_dept_id_list');
    }


    /**
     * 获取部门列表
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.CJnzLl&treeId=385&articleId=106817&docType=1#s0
     *
     * @param int $parentDepartmentId
     * @param bool $fetchChild
     * @param string $lang
     * @return mixed
     * @throws \think\exception\DbException
     */
    public function departmentList($parentDepartmentId = 1, $fetchChild = true, $lang = 'zh_CN') {
        $response = $this->callWithToken("department/list", [
            'id' => $parentDepartmentId,
            'fetch_child' => $fetchChild ? "true" : "false",
            'lang' => $lang
        ]);
        return $this->_fetch($response, 'department');
    }

    /**
     * 获取部门详情
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.CJnzLl&treeId=385&articleId=106817&docType=1#s1
     *
     * @param $departmentId
     * @param string $lang
     * @return mixed
     * @throws \think\exception\DbException
     */
    public function departmentGet($departmentId, $lang = 'zh_CN') {
        $response = $this->callWithToken("department/get", [
            'id' => $departmentId,
            'lang' => $lang
        ]);
        return $response;
    }

    /**
     * 创建部门
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.CJnzLl&treeId=385&articleId=106817&docType=1#s2
     *
     * @param DepartmentSaveRequest $request
     * @return bool|string 部门id
     * @throws \think\exception\DbException
     * @throws \ReflectionException
     */
    public function departmentCreate(DepartmentSaveRequest $request) {
        $data = $request->toData();
        $response = $this->callWithToken("department/create", $data, true);
        return $this->_fetch($response, "id");
    }

    /**
     * 更新部门
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.CJnzLl&treeId=385&articleId=106817&docType=1#s3
     *
     * @param DepartmentSaveRequest $request
     * @return bool|string 部门id
     * @throws \think\exception\DbException
     * @throws \ReflectionException
     */
    public function departmentUpdate(DepartmentSaveRequest $request) {
        $data = $request->toData();
        $response = $this->callWithToken("department/update", $data, true);
        return $this->_fetch($response, "id");
    }

    /**
     * 删除部门
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.CJnzLl&treeId=385&articleId=106817&docType=1#s4
     *
     * @param $departmentId
     * @return bool
     * @throws \think\exception\DbException
     */
    public function departmentDelete($departmentId) {
        $response = $this->callWithToken("department/delete", ['id' => $departmentId]);
        return $this->_fetch($response, "errmsg") == 'ok';
    }

    /**
     * 查询部门的所有上级父部门路径
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.CJnzLl&treeId=385&articleId=106817&docType=1#s5
     *
     * @param $departmentId
     * @return mixed
     * @throws \think\exception\DbException
     */
    public function departmentListParentDeptsByDept($departmentId) {
        $response = $this->callWithToken("department/list_parent_depts_by_dept", ['id' => $departmentId]);
        return $this->_fetch($response, "parentIds");
    }


    /**
     * 查询指定用户的所有上级父部门路径
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.CJnzLl&treeId=385&articleId=106817&docType=1#s6
     *
     * @param $userId
     * @return mixed
     * @throws \think\exception\DbException
     */
    public function departmentListParentDepts($userId) {
        $response = $this->callWithToken("department/list_parent_depts", ['userId' => $userId]);
        return $this->_fetch($response, "department");
    }

    /**
     * 企业通讯录员工搜索
     * https://open-doc.dingtalk.com/docs/api.htm?spm=a219a.7395905.0.0.W0A4Tb&apiId=27910
     *
     * @param $keyword
     * @param int $offset
     * @param int $size
     * @param boolean $clearRead
     * @return array|mixed
     * {
     *      "total": 2,
     *      "hasMore": false,
     *      "list": [{
     *          "name": "<red>姓名<\/red>",
     *          "title": "研发总监",
     *          "nick": "",
     *          "searchRank": 10026.086022563,
     *          "email": "",
     *          "mobile": "188965xxxxx",
     *          "hasSubordinate": false,
     *          "extFields": "名字:<red>姓名<\/red>",
     *          "depts": [{
     *              "memberCount": 10,
     *              "name": "部门名称",
     *              "id": 14332723,
     *              "deptVisible": {
     *                  "hidePermitDeptIds": [],
     *                  "outerPermitUids": [],
     *                  "outerPermitDeptIds": [],
     *                  "hidePermitUids": []
     *              }
     *          }],
     *          "staffId": "userid"
     *      }]
     * }
     * @throws \think\exception\DbException
     */
    public function corpEmpSearch($keyword, $offset = 0, $size = 10, $clearRead = true) {
        $response = $this->callTop("dingtalk.corp.emp.search", [
            'keyword' => $keyword,
            'offset' => $offset,
            'size' => $size
        ]);
        if ($clearRead && isset($response['list'])) {
            foreach ($response['list'] as &$item) {
                $item['name'] = str_replace("<red>", "", $item['name']);
                $item['name'] = str_replace("</red>", "", $item['name']);
                $item['extFields'] = str_replace("<red>", "", $item['extFields']);
                $item['extFields'] = str_replace("</red>", "", $item['extFields']);
            }
        }
        return $response;
    }

    /**
     * 通过CODE换取用户身份
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.gGrEeD&treeId=385&articleId=104969&docType=1#s0
     *  {
     *      "userid": "USERID",     //员工在企业内的UserID
     *      "deviceId":"DEVICEID",  //手机设备号,由钉钉在安装时随机产生
     *      "is_sys": true,         //是否是管理员
     *      "sys_level": 0|1|2      //级别，0：非管理员 1：超级管理员（主管理员） 2：普通管理员（子管理员） 100：老板
     *  }
     * @param $code
     * @return mixed
     * @throws \think\exception\DbException
     */
    public function userGetuserinfo($code) {
        $response = $this->callWithToken("user/getuserinfo", ['code' => $code]);
        return $response;
    }

    /**
     * 通过userid获取成员详情信息
     * {
     *       "errcode": 0,
     *       "errmsg": "ok",
     *       "openId": "PiiiPyQqBNBii0HnCJ3zljcuAiEiE",     //在本 服务窗运营服务商 范围内,唯一标识关注者身份的id（不可修改）
     *       "roles": [{                             //角色信息（ISV不可见），json数组格式
     *           "id": 23003585,                     //角色id（ISV不可见）
     *           "name": "财务",                      //角色名称（ISV不可见）
     *           "groupName": "职务"                  //角色分组名称（ISV不可见）
     *       }],
     *       "remark": "备注",                         //备注（ISV不可见）
     *       "userid": "04232334556237185",
     *       "isLeaderInDepts": "{1:false}",          //在对应的部门中是否为主管, Map结构的json字符串, key是部门的Id, value是人员在这个部门中是否为主管, true表示是, false表示不是
     *       "isBoss": false,                         //是否为企业的老板, true表示是, false表示不是
     *       "hiredDate": 1520265600000,              //入职时间
     *       "isSenior": false,                       //是否高管
     *       "tel": "010-88996533",                   //分机号（仅限企业内部开发调用）
     *       "department": [1,2],                     //成员所属部门id列表
     *       "workPlace": "北京市朝阳区",              //办公地点（ISV不可见）
     *       "email": "ceshi@aliyun.com",            //员工的电子邮箱（ISV不可见）
     *       "orgEmail": "ceshi@aliyun.com",         //员工的企业邮箱，如果员工已经开通了企业邮箱，接口会返回，否则不会返回（ISV不可见）
     *       "orderInDepts": "{1:71738366882504}",   //在对应的部门中的排序, Map结构的json字符串, key是部门的Id, value是人员在这个部门的排序值
     *       "dingId": "$:LWCP_v1:$aTPvVHhhsCMtDZRQ1xbYGg==",   //钉钉Id,在钉钉全局范围内标识用户的身份（不可修改）。
     *       "unionid": "",                     //在当前isv全局范围内唯一标识一个用户的身份,用户无法修改
     *       "mobile": "15901516821",           //手机号码（ISV不可见）
     *       "active": false,                   //是否已经激活, true表示已激活, false表示未激活
     *       "avatar": "dingtalk.com/abc.jpg",  //头像url
     *       "isAdmin": false,                  //是否为企业的管理员, true表示是, false表示不是
     *       "isHide": false,                   //是否号码隐藏, true表示隐藏, false表示不隐藏
     *       "jobnumber": "001",                //员工工号
     *       "name": "测试名字",                 //成员名称
     *       "extattr": {},                     //扩展属性，可以设置多种属性(但手机上最多只能显示10个扩展属性，具体显示哪些属性，请到OA管理后台->设置->通讯录信息设置和OA管理后台->设置->手机端显示信息设置)
     *       "stateCode": "86",                 //手机号码区号
     *       "position": "总监"                  //职位信息
     * }
     * @param $userid
     * @param string $lang
     * @return array|boolean
     * @throws \think\exception\DbException
     */
    public function userGet($userid, $lang = 'zh_CN') {
        $response = $this->callWithToken("user/get", ['userid' => $userid, 'lang' => $lang]);
        if ($this->isError($response)) {
            return false;
        }
        return $response;
    }

    /**
     * 创建企业内部成员
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.eJpKSb&treeId=385&articleId=106816&docType=1#s2
     *
     * @param UserSaveRequest $request
     * @return bool|string 用户id
     * @throws \think\exception\DbException
     * @throws \ReflectionException
     */
    public function userCreate(UserSaveRequest $request) {
        $data = $request->toData();
        $response = $this->callWithToken("user/create", $data, true);
        return $this->_fetch($response, "userid");
    }

    /**
     * 更新企业成员信息
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.eJpKSb&treeId=385&articleId=106816&docType=1#s3
     *
     * @param UserSaveRequest $request
     * @return bool
     * @throws \ReflectionException
     * @throws \think\exception\DbException
     */
    public function userUpdate(UserSaveRequest $request) {
        $data = $request->toData();
        $response = $this->callWithToken("user/update", $data, true);
        return $this->_fetch($response, "errcode") === 0;
    }

    /**
     * 删除企业内部用户
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.eJpKSb&treeId=385&articleId=106816&docType=1#s4
     *
     * @param $userid
     * @return bool
     * @throws \think\exception\DbException
     */
    public function userDelete($userid) {
        $response = $this->callWithToken("user/delete", ['userid' => $userid]);
        return $this->_fetch($response, "errcode") === 0;
    }

    /**
     * 批量删除企业内部用户
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.eJpKSb&treeId=385&articleId=106816&docType=1#s5
     *
     * @param array $useridlist
     * @return bool
     * @throws \think\exception\DbException
     */
    public function userBatchdelete(array $useridlist) {
        $response = $this->callWithToken("user/batchdelete", ['useridlist' => $useridlist], true);
        return $this->_fetch($response, "errcode") === 0;
    }

    /**
     * 获取部门成员列表
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.eJpKSb&treeId=385&articleId=106816&docType=1#s6
     *
     * @param int $departmentId 获取的部门id
     * @param int $offset 支持分页查询，与size参数同时设置时才生效，此参数代表偏移量
     * @param int $size 支持分页查询，与offset参数同时设置时才生效，此参数代表分页大小，最大100
     * @param string $order 支持分页查询，部门成员的排序规则，默认不传是按自定义排序；
     *                                  entry_asc代表按照进入部门的时间升序，
     *                                  entry_desc代表按照进入部门的时间降序，
     *                                  modify_asc代表按照部门信息修改时间升序，
     *                                  modify_desc代表按照部门信息修改时间降序，
     *                                  custom代表用户定义(未定义时按照拼音)排序
     * @param string $lang 通讯录语言(默认zh_CN另外支持en_US)
     * @return array
     * {
     *      "errcode": 0,
     *      "errmsg": "ok",
     *      "hasMore": false,
     *      "userlist": [
     *          {
     *              "userid": "zhangsan",
     *              "name": "张三"
     *          }
     *      ]
     * }
     * @throws \think\exception\DbException
     */
    public function userSimplelist($departmentId, $offset = 0, $size = 100, $order = 'custom', $lang = 'zh_CN') {
        $response = $this->callWithToken("user/simplelist", [
            'department_id' => $departmentId,
            'offset' => $offset,
            'size' => $size,
            'order' => $order,
            'lang' => $lang
        ]);

        return $response;
    }

    /**
     * 获取部门成员列表（详细）
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.eJpKSb&treeId=385&articleId=106816&docType=1#s6
     *
     * @param int $departmentId 获取的部门id
     * @param int $offset 支持分页查询，与size参数同时设置时才生效，此参数代表偏移量
     * @param int $size 支持分页查询，与offset参数同时设置时才生效，此参数代表分页大小，最大100
     * @param string $order 支持分页查询，部门成员的排序规则，默认不传是按自定义排序；
     *                                  entry_asc代表按照进入部门的时间升序，
     *                                  entry_desc代表按照进入部门的时间降序，
     *                                  modify_asc代表按照部门信息修改时间升序，
     *                                  modify_desc代表按照部门信息修改时间降序，
     *                                  custom代表用户定义(未定义时按照拼音)排序
     * @param string $lang 通讯录语言(默认zh_CN另外支持en_US)
     * @return array
     * {
     *      "errcode": 0,
     *      "errmsg": "ok",
     *      "hasMore": false,
     *      "userlist": [
     *          {
     *               "userid": "zhangsan",
     *               "dingId": "dwdded",
     *               "unionid": "PiiiPyQqBNBii0HnCJ3zljcuAiEiE",
     *               "mobile": "13122222222",
     *               "tel" : "010-123333",
     *               "workPlace" :"",
     *               "remark" : "",
     *               "order" : 1,
     *               "isAdmin": true,
     *               "isBoss": false,
     *               "isHide": true,
     *               "isLeader": true,
     *               "name": "张三",
     *               "active": true,
     *               "department": [1, 2],
     *               "position": "工程师",
     *               "email": "zhangsan@alibaba-inc.com",
     *               "avatar":  "./dingtalk/abc.jpg",
     *               "jobnumber": "111111",
     *               "extattr": {
     *                      "爱好":"旅游",
     *                      "年龄":"24"
     *               }
     *          }
     *      ]
     * }
     * @throws \think\exception\DbException
     */
    public function userList($departmentId, $offset = 0, $size = 100, $order = 'custom', $lang = 'zh_CN') {
        $response = $this->callWithToken("user/list", [
            'department_id' => $departmentId,
            'offset' => $offset,
            'size' => $size,
            'order' => $order,
            'lang' => $lang
        ]);

        return $response;
    }

    /**
     * 获取企业员工总数
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.AhsZTW&treeId=385&articleId=105485&docType=1
     *
     * @param int $onlyActive 0：包含未激活钉钉的人员数量 1：不包含未激活钉钉的人员数量
     * @return bool|int
     * @throws \think\exception\DbException
     */
    public function userGetOrgUserCount($onlyActive = 0) {
        $response = $this->callWithToken("user/get_org_user_count", ['onlyActive' => $onlyActive]);
        return $this->_fetch($response, 'count');
    }

    /**
     * 根据unionid获取成员的userid
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7386797.0.0.uYGXlw&source=search&treeId=371&articleId=106816&docType=1#s9
     *
     * @param $unionid  string 用户在当前钉钉开放平台账号范围内的唯一标识，同一个钉钉开放平台账号可以包含多个开放应用，同时也包含ISV的套件应用及企业应用
     * @return bool|string 返回userid
     * @throws \think\exception\DbException
     */
    public function userGetUseridByUnionid($unionid) {
        $response = $this->callWithToken("user/getUseridByUnionid", [
            "unionid" => $unionid
        ], false);
        return $this->_fetch($response, "userid");
    }

    /**
     * 获取管理员列表
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.eJpKSb&treeId=385&articleId=106816&docType=1#s8
     *
     * @return bool|array
     * @throws \think\exception\DbException
     */
    public function userGetAdmin() {
        $response = $this->callWithToken("user/get_admin");
        return $this->_fetch($response, "admin_list");
    }

    /**
     * 获取管理员的微应用管理权限
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.eJpKSb&treeId=385&articleId=106816&docType=1#s9
     *
     * @param $appId
     * @param $userId
     * @return bool
     * @throws \think\exception\DbException
     */
    public function userCanAccessMicroapp($appId, $userId) {
        $response = $this->callWithToken("user/can_access_microapp", ['appId' => $appId, 'userId' => $userId]);
        return $this->_fetch($response, "canAccess");
    }

    /**
     * 向自定义机器人发送消息
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.JZVIOw&treeId=257&articleId=105735&docType=1
     *
     * @param $robotAccessToken
     * @param RobotMsg $robotMsg
     * @return array
     * @throws \think\exception\DbException
     */
    public function robotSend($robotAccessToken, RobotMsg $robotMsg) {
        $response = $this->call("robot/send", [
            'access_token' => $robotAccessToken
        ], $robotMsg->toMsg(), true);

        return $response;
    }


    /**
     * 创建会话（群）
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.YYBYt6&treeId=374&articleId=104977&docType=1#s1
     *
     * @param $name
     * @param $owner
     * @param array $useridlist
     * @param int $showHistoryType
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function chatCreate($name, $owner, array $useridlist, $showHistoryType = 0) {
        $response = $this->callWithToken("chat/create", [
            'name' => $name,
            'owner' => $owner,
            'useridlist' => $useridlist,
            'showHistoryType' => $showHistoryType
        ], true);

        return $this->_fetch($response, "chatid");
    }

    /**
     * 修改会话（群）信息
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.YYBYt6&treeId=374&articleId=104977&docType=1#s2
     *
     * @param $chatid
     * @param bool $name
     * @param bool $owner
     * @param array $add_useridlist
     * @param array $del_useridlist
     * @return bool
     * @throws \think\exception\DbException
     */
    public function chatUpdate($chatid, $name = false, $owner = false, array $add_useridlist = [], array $del_useridlist = []) {
        $data = ['chatid' => $chatid];
        if (!empty($name)) {
            $data['name'] = $name;
        }
        if (!empty($owner)) {
            $data['owner'] = $owner;
        }
        if (!empty($add_useridlist)) {
            $data['add_useridlist'] = $add_useridlist;
        }
        if (!empty($del_useridlist)) {
            $data['del_useridlist'] = $del_useridlist;
        }
        if (empty($data)) {
            return false;
        }
        $response = $this->callWithToken("chat/update", $data, true);

        return $this->_fetch($response, "errmsg") == 'ok';
    }


    /**
     * 获取会话（群）信息
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.YYBYt6&treeId=374&articleId=104977&docType=1#s3
     * {
     *     "name": "群名称",
     *     "owner": "zhangsan",
     *     "useridlist": ["zhangsan","lisi"]
     * }
     *
     * @param $chatid
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function chatGet($chatid) {
        $response = $this->callWithToken("chat/get", ['chatid' => $chatid], false);
        return $this->_fetch($response, "chat_info");
    }

    /**
     * 发送群消息
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.YYBYt6&treeId=374&articleId=104977&docType=1#s4
     *
     * @param $chatid
     * @param ChatMsg $chatMsg
     * @return string $messageId
     * @throws \think\exception\DbException
     */
    public function chatSend($chatid, ChatMsg $chatMsg) {
        $data = $chatMsg->toMsg();
        $data['chatid'] = $chatid;
        $response = $this->callWithToken("chat/send", $data, true);
        return $this->_fetch($response, "messageId");
    }


    /**
     * 查询群消息已读人员列表
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.YYBYt6&treeId=374&articleId=104977&docType=1#s5
     *
     * @param string $messageId 发送群消息接口返回的加密消息id
     * @param int $cursor 分页查询的游标，第一次可以传0，后续传返回结果中的next_cursor的值。当返回结果中，没有next_cursor时，表示没有后续的数据了，可以结束调用
     * @param int $size 分页查询的大小，最大可以传100
     * @return array
     * @throws \think\exception\DbException
     */
    public function chatGetReadList($messageId, $cursor = 0, $size = 100) {
        $messageId = urlencode($messageId);
        $response = $this->callWithToken("chat/getReadList", [
            'messageId' => $messageId,
            'cursor' => $cursor,
            'size' => $size
        ], false);
        return $response;
    }

    /**
     * 发送普通消息
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.uvrnQ3&treeId=385&articleId=104974&docType=1
     *
     * @param $sender string 消息发送者userId
     * @param $chatid string 群消息或者个人聊天会话Id
     * @param ChatMsg $chatMsg
     * @return bool|mixed  如果是群，返回跟发送者同一家企业的一组工号；如果是个人聊天，只返回发送者同一家企业的一个工号；不在同一家企业，发送失败
     * @throws \think\exception\DbException
     */
    public function messageSendToConversation($sender, $chatid, ChatMsg $chatMsg) {
        $data = $chatMsg->toMsg();
        $data['sender'] = $sender;
        $data['cid'] = $chatid;

        $response = $this->callWithToken("message/send_to_conversation", $data, true);
        return $this->_fetch($response, "receiver");
    }


    /**
     * 企业会话消息异步发送(接收方收到的消息上的联系人头像是微应用)
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.T5tJwp&treeId=385&articleId=28915&docType=2
     *
     * @param ChatMsg $chatMsg
     * @param bool $toAllUser 是否发送给企业全部用户，默认false
     * @param array $deptIdList 接收者的部门id列表，最大列表长度：20
     * @param array $useridList 接收者的用户userid列表，最大列表长度：20
     * @return string|bool 创建的异步发送任务id
     * @throws \think\exception\DbException
     */
    public function corpMessageCorpconversationAsyncsend(ChatMsg $chatMsg, $toAllUser = false, $deptIdList = null, $useridList = null) {
        $data = [];
        $data['agent_id'] = $this->getAgentId();
        $data['msgtype'] = $chatMsg->getMsgtype();
        $data['msgcontent'] = json_encode($chatMsg->getMsgContent());
        if (!empty($useridList)) {
            $data['userid_list'] = join(',', $useridList);
        }
        if (!empty($deptIdList)) {
            $data['dept_id_list'] = join(",", $deptIdList);
        }
        $data['to_all_user'] = $toAllUser ? 'true' : 'false';
        $response = $this->callTop("dingtalk.corp.message.corpconversation.asyncsend", $data);
        return $this->_fetch($response, 'task_id');
    }

    /**
     * 企业会话消息异步发送(接收方收到的消息上的联系人头像是code对应的用户)
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.wGO98X&treeId=385&articleId=28919&docType=2
     *
     * @param string $code 通过jsapi requestOperateAuthCode获取到的用户授权码
     * @param ChatMsg $chatMsg
     * @param bool $toAllUser 是否发送给企业全部用户，默认false
     * @param array $deptIdList 接收者的部门id列表，最大列表长度：20
     * @param array $useridList 接收者的用户userid列表，最大列表长度：20
     * @return string|bool 创建的异步发送任务id
     * @throws \think\exception\DbException
     */
    public function corpMessageCorpconversationAsyncsendbycode($code, ChatMsg $chatMsg, $toAllUser = false, $deptIdList = null, $useridList = null) {
        $data = [];
        $data['code'] = $code;
        $data['agent_id'] = $this->getAgentId();
        $data['msgtype'] = $chatMsg->getMsgtype();
        $data['msgcontent'] = json_encode($chatMsg->getMsgContent());
        if (!empty($useridList)) {
            $data['userid_list'] = join(',', $useridList);
        }
        if (!empty($deptIdList)) {
            $data['dept_id_list'] = join(",", $deptIdList);
        }
        $data['to_all_user'] = $toAllUser ? 'true' : 'false';
        $response = $this->callTop("dingtalk.corp.message.corpconversation.asyncsendbycode", $data);
        return $this->_fetch($response, 'task_id');
    }


    /**
     * 获取异步发送企业会话消息的发送进度
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.oGCq9L&treeId=385&articleId=28917&docType=2
     *
     * @param $taskId
     * @return bool|array
     * {
     *      "progress_in_percent": 100, //取值 0-100，表示处理的百分比
     *      "status": 2 //任务执行状态,0=未开始,1=处理中,2=处理完毕
     * }
     * @throws \think\exception\DbException
     */
    public function corpMessageCorpconversationGetsendprogress($taskId) {
        $data = [];
        $data['agent_id'] = $this->getAgentId();
        $data['task_id'] = $taskId;
        $response = $this->callTop("dingtalk.corp.message.corpconversation.getsendprogress", $data);
        return $this->_fetch($response, 'progress');
    }

    /**
     * 获取异步向企业会话发送消息的结果
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.XGYXrz&treeId=385&articleId=28918&docType=2
     *
     * @param $taskId
     * @return bool|array
     * {
     *      "failed_user_id_list": [],
     *      "forbidden_user_id_list": [],
     *      "invalid_dept_id_list": [],
     *      "invalid_user_id_list": [],
     *      "read_user_id_list": [],
     *      "unread_user_id_list": []
     * }
     * @throws \think\exception\DbException
     */
    public function corpMessageCorpconversationGetsendresult($taskId) {
        $data = [];
        $data['agent_id'] = $this->getAgentId();
        $data['task_id'] = $taskId;
        $response = $this->callTop("dingtalk.corp.message.corpconversation.getsendresult", $data);
        return $this->_fetch($response, 'send_result');
    }


    /**
     * 获取微应用后台免登的SsoToken
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7386797.0.0.0hPDLH&source=search&treeId=168&articleId=104880&docType=1#s2
     *
     * @return bool|string
     * @throws \think\exception\DbException
     */
    public function ssoGettoken() {
        // 先到缓存中获取
        $response = $this->getCache("ssoToken", 7000);
        if (!empty($response) && !empty($response['access_token'])) {
            return $response['access_token'];
        }

        // 调用接口获取
        $response = $this->callWithCorp("sso/gettoken");
        if (!empty($response) && !empty($response['access_token'])) {
            $this->putCache('token', $response, 7000);
        }
        $this->token = $this->_fetch($response, 'access_token');
        return $this->token;
    }

    /**
     * 通过CODE换取微应用管理员的身份信息
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.aMQN8g&treeId=385&articleId=104969&docType=1#s1
     *
     * @param $code
     * @return array
     * {
     *   "corp_info": {
     *      "corp_name": "一家公司",              //公司名字
     *      "corpid": "dingxxxxxx"               //公司corpid
     *   },
     *   "errcode": 0,
     *   "errmsg": "ok",
     *   "is_sys": true,                        //是否是管理员
     *   "user_info": {
     *      "avatar": "http://xxxxxxx.jpg",
     *      "email": "123456@aliyun.com",
     *      "name": "名称",
     *      "userid": "0571"
     *   }
     * }
     * @throws \think\exception\DbException
     */
    public function ssoGetuserinfo($code) {
        $response = $this->call("sso/getuserinfo", [
            'access_token' => $this->ssoGettoken(),
            'code' => $code
        ]);

        return $response;
    }

    /**
     * 新建微应用
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.xehngy&treeId=385&articleId=104978&docType=1#s0
     *
     * @param MicroappSaveRequest $request
     * @return bool|string 微应用id
     * @throws \ReflectionException
     * @throws \think\exception\DbException
     */
    public function microappCreate(MicroappSaveRequest $request) {
        $data = $request->toData();
        $response = $this->callWithToken("microapp/create", $data, true);
        return $this->_fetch($response, 'agentid');
    }

    /**
     * 更新微应用
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.xehngy&treeId=385&articleId=104978&docType=1#s1
     *
     * @param MicroappSaveRequest $request
     * @return bool|string 微应用id
     * @throws \ReflectionException
     * @throws \think\exception\DbException
     */
    public function microappUpdate(MicroappSaveRequest $request) {
        $data = $request->toData();
        $response = $this->callWithToken("microapp/update", $data, true);
        return $this->_fetch($response, 'agentId');
    }

    /**
     * 删除微应用
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.xehngy&treeId=385&articleId=104978&docType=1#s2
     *
     * @param $agentId
     * @return bool
     * @throws \think\exception\DbException
     */
    public function microappDelete($agentId) {
        $response = $this->callWithToken("microapp/delete", [
            'agentId' => intval($agentId)
        ], true);
        return $this->_fetch($response, 'errcode') === 0;
    }

    /**
     * 微应用列表
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.xehngy&treeId=385&articleId=104978&docType=1#s3
     *
     * @return bool|array
     * {
     *   "appIcon": "http://static.dingtalk.com/media/szIzMg_200_200.png",
     *   "agentId": 111,
     *   "appDesc": "微应用描述",
     *   "isSelf": true,
     *   "name": "微应用名称",
     *   "homepageLink": "http://oa.dingtalk.com/?h5",
     *   "pcHomepageLink": "http://oa.dingtalk.com/?pc",
     *   "appStatus": 1,
     *   "ompLink": "http://oa.dingtalk.com/index"
     * }
     * @throws \think\exception\DbException
     */
    public function microappList() {
        $response = $this->callWithToken("microapp/list", [], true);
        return $this->_fetch($response, 'appList');
    }

    /**
     * 用户可见的微应用列表
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.xehngy&treeId=385&articleId=104978&docType=1#s4
     *
     * @param $userid
     * @return bool|array
     * {
     *   "appIcon": "http://static.dingtalk.com/media/szIzMg_200_200.png",
     *   "agentId": 111,
     *   "appDesc": "微应用描述",
     *   "isSelf": true,
     *   "name": "微应用名称",
     *   "homepageLink": "http://oa.dingtalk.com/?h5",
     *   "pcHomepageLink": "http://oa.dingtalk.com/?pc",
     *   "appStatus": 1,
     *   "ompLink": "http://oa.dingtalk.com/index"
     * }
     * @throws \think\exception\DbException
     */
    public function microappListByUserid($userid) {
        $response = $this->callWithToken("microapp/list_by_userid", [
            'userid' => $userid
        ], false);
        return $this->_fetch($response, 'appList');
    }

    /**
     * 获取企业设置的微应用可见范围
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.xehngy&treeId=385&articleId=104978&docType=1#s5
     *
     * @param $agentId
     * @return array
     * {
     *   "errcode": 0,
     *   "errmsg": "ok",
     *   "isHidden":false,    //是否仅限管理员可见
     *   "deptVisibleScopes": [1265,9634],  //微应用的可见部门id列表，格式为JSON数组
     *   "userVisibleScopes": ["manager6577","766578"] //微应用的可见用户id列表，格式为JSON数组
     * }
     * @throws \think\exception\DbException
     */
    public function microappVisibleScopes($agentId) {
        $response = $this->callWithToken("microapp/visible_scopes", [
            'agentId' => $agentId
        ], true);
        return $response;
    }

    /**
     * 设置微应用可见范围
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.xehngy&treeId=385&articleId=104978&docType=1#s6
     *
     * @param $agentId
     * @param bool $isHidden
     * @param array $deptVisibleScopes
     * @param array $userVisibleScopes
     * @return bool
     * @throws \think\exception\DbException
     */
    public function microappSetVisibleScopes($agentId, $isHidden = null, $deptVisibleScopes = null, $userVisibleScopes = null) {
        $data = [
            'agentId' => $agentId
        ];
        if ($isHidden != null) {
            $data['isHidden'] = $isHidden;
        }
        if ($deptVisibleScopes != null) {
            $data['deptVisibleScopes'] = $deptVisibleScopes;
        }
        if ($userVisibleScopes != null) {
            $data['userVisibleScopes'] = $userVisibleScopes;
        }
        $response = $this->callWithToken("microapp/set_visible_scopes", $data, true);
        return $this->_fetch($response, 'errcode') === 0;
    }

    /**
     * 上传媒体文件
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.BI3RuU&treeId=385&articleId=104971&docType=1#s0
     *
     * @param $mediaFile
     * @param string $type 媒体文件类型，分别有图片（image）、语音（voice）、普通文件(file)
     * @param string $fileName
     * @return array
     * {
     *   "errcode": 0,
     *   "errmsg": "ok",
     *   "type": "image",
     *   "media_id": "@dsa8d87y7c8d8c",
     *   "created_at":1442027997327
     * }
     * @throws \think\exception\DbException
     */
    public function mediaUpload($mediaFile, $type, $fileName = "") {
        if (!class_exists("\CURLFile")) {
            //CURLFile只有php5.5及其以上才支持
            $this->lastStatus = [
                'errcode' => -99999,
                'errmsg' => 'CURLFile not found.'
            ];
            return false;
        }
        if (!($mediaFile instanceof \CURLFile)) {
            $mediaFile = new \CURLFile($mediaFile, "", $fileName);
        }
        $response = $this->call("media/upload", [
            'access_token' => $this->gettoken(),
            'type' => $type
        ], [
            'media' => $mediaFile
        ], true, true);

        return $response;
    }

    /**
     * 获取媒体文件
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.BI3RuU&treeId=385&articleId=104971&docType=1#s1
     *
     * @param $mediaId
     * @return mixed|string
     * @throws \think\exception\DbException
     */
    public function mediaDownloadFile($mediaId) {
        $apiName = "media/downloadFile";
        if (!$this->_authApi($apiName)) {
            $this->lastStatus = [
                'errcode' => 777777,
                'errmsg' => 'api ' . $apiName . ' auth faied.'
            ];
            return $this->lastStatus;
        }
        $url = self::OAPI_URL . $apiName . "?access_token=" . $this->gettoken() . "&media_id=" . $mediaId;
        return Http::get($url);
    }

    /**
     * 发送文件给指定用户
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.NHDydC&treeId=385&articleId=104970&docType=1#s0
     *
     * @param string $userid 企业用户userid
     * @param string $mediaId 此处的mediaId必须是fileUploadSingle上传返回的media_id
     * @param string $fileName
     * @return bool
     * @throws \think\exception\DbException
     */
    public function cspaceAddToSingleChat($userid, $mediaId, $fileName) {
        $response = $this->call("cspace/add_to_single_chat", [
            'access_token' => $this->gettoken(),
            'agent_id' => $this->getAgentId(),
            'userid' => $userid,
            'media_id' => urlencode($mediaId),
            'file_name' => urlencode($fileName)
        ], [], true);

        return $this->_fetch($response, 'errcode') === 0;
    }

    /**
     * 将上传的文件放到指定用户的钉盘里。
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.NHDydC&treeId=385&articleId=104970&docType=1#s1
     *
     * @param string $code 如果是微应用，code值为微应用免登授权码,如果是服务窗应用，code值为服务窗免登授权码。code为临时授权码，只能消费一次，下次请求需要重新获取新的code。
     * @param string $mediaId 调用钉盘上传文件接口得到的mediaid。此处的mediaId必须是fileUploadSingle上传返回的media_id
     * @param string $spaceId 调用云盘选择控件后获取的用户钉盘空间ID
     * @param string $folderId 调用云盘选择控件后获取的用户钉盘文件夹ID
     * @param string $name 上传文件的名称，不能包含非法字符
     * @param bool $overwrite 遇到同名文件是否覆盖，若不覆盖，则会自动重命名本次新增的文件，默认为false
     * @return bool|array|string
     * @throws \think\exception\DbException
     */
    public function cspaceAdd($code, $mediaId, $spaceId, $folderId, $name, $overwrite = false) {
        $response = $this->callWithToken("cspace/add", [
            'agent_id' => $this->getAgentId(),
            'code' => $code,
            'media_id' => urlencode($mediaId),
            'space_id' => $spaceId,
            'folder_id' => $folderId,
            'name' => urlencode($name),
            'overwrite' => $overwrite
        ]);
        $dentryStr = $this->_fetch($response, 'dentry');
        if (!$dentryStr) {
            return $dentryStr;
        }
        $dentry = json_decode($dentryStr, true);
        if (!$dentry) {
            return $dentry;
        }
        return $dentryStr;
    }

    /**
     * 获取企业下的自定义空间
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.NHDydC&treeId=385&articleId=104970&docType=1#s2
     *
     * @param bool $domain 企业调用时传入，授权访问该domain的自定义空间，ISV调用传入false
     * @return bool|string 空间id
     * @throws \think\exception\DbException
     */
    public function cspaceGetCustomSpace($domain) {
        $args = [];
        if ($domain) {
            $args['domain'] = $domain;
        } else {
            $args['agent_id'] = $this->getAgentId();
        }
        $response = $this->callWithToken("cspace/get_custom_space", $args);
        return $this->_fetch($response, "spaceid");
    }


    /**
     * 授权用户访问企业下的自定义空间
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.NHDydC&treeId=385&articleId=104970&docType=1#s3
     *
     * @param string $domain 企业调用时传入，授权访问该domain的自定义空间，ISV调用传入false
     * @param string $userid 企业用户userid
     * @param string $type 权限类型，目前支持上传和下载，上传请传add，下载请传download
     * @param string|array $pathOrFileids 授权访问路径或者授权访问的文件id列表
     *                                      如果传入的是string，代表授权访问的路径，如授权访问所有文件传“/”，授权访问/doc文件夹传“/doc/”。
     *                                      如果传入的是array，代表授权访问的文件id列表
     * @param int $duration 权限有效时间，有效范围为0~3600秒，超出此范围或不传默认为30秒
     * @return bool
     * @throws \think\exception\DbException
     */
    public function cspaceGrantCustomSpace($domain, $userid, $type, $pathOrFileids, $duration = 30) {
        $args = [];
        if ($domain) {
            $args['domain'] = $domain;
        } else {
            $args['agent_id'] = $this->getAgentId();
        }
        $args['userid'] = $userid;
        $args['type'] = $type;
        if (is_array($pathOrFileids)) {
            $args['fileids'] = join(',', $pathOrFileids);
        } else {
            $args['path'] = urlencode($pathOrFileids);
        }
        $args['duration'] = $duration;

        $response = $this->callWithToken("cspace/grant_custom_space", $args);
        return $this->_fetch($response, "errcode") === 0;
    }

    /**
     * 开启文件分块上传事务
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.NHDydC&treeId=385&articleId=104970&docType=1#s4
     *
     * @param int $fileSize 文件大小
     * @param int $chunkNumbers 文件总块数
     * @return bool|string 上传upload_id
     * @throws \think\exception\DbException
     */
    public function startFileUploadTransaction($fileSize, $chunkNumbers) {
        $response = $this->callWithToken("file/upload/transaction", [
            'agent_id' => $this->getAgentId(),
            'file_size' => $fileSize,
            'chunk_numbers' => $chunkNumbers
        ]);
        return $this->_fetch($response, "upload_id");
    }

    /**
     * 单步文件上传
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.ZSCrQD&treeId=373&articleId=104970&docType=1#s7
     *
     * @param string $filePath 文件路径，文件大小不得超过8M
     * @return bool|string 上传文件的media_id
     * @throws \think\exception\DbException
     */
    public function fileUploadSingle($filePath) {
        $args = [];
        $args['access_token'] = $this->gettoken();
        $args['agent_id'] = $this->getAgentId();
        $args['file_size'] = filesize($filePath);
        $response = $this->call("file/upload/single", $args, [
            'file' => new \CURLFile($filePath)
        ], true, true);
        return $this->_fetch($response, "media_id");
    }

    /**
     * 获取角色的员工列表
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.tsXw9u&treeId=385&articleId=29204&docType=2
     *
     * @param $roleId
     * @param int $size
     * @param int $offset
     * @return array|mixed
     * {
     *      "has_more": false,
     *      "list": [
     *          {
     *              "userid": "0011"
     *          }
     *      ]
     * }
     * @throws \think\exception\DbException
     */
    public function corpRoleSimplelist($roleId, $size = 20, $offset = 0) {
        $response = $this->callTop("dingtalk.corp.role.simplelist", [
            'role_id' => $roleId,
            'size' => $size,
            'offset' => $offset
        ]);
        return $response;
    }

    /**
     * 获取企业角色列表
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.lRGpEb&treeId=385&articleId=29205&docType=2
     *
     * @param int $size
     * @param int $offset
     * @return array|mixed
     * {
     *     "has_more": "false",
     *     "list": [
     *         {
     *             "group_name": "职务",
     *             "roles": [
     *                 {
     *                     "role_name": "财务",
     *                     "id": 123
     *                 }
     *             ]
     *         }
     *     ]
     * }
     * @throws \think\exception\DbException
     */
    public function corpRoleList($size = 20, $offset = 0) {
        $response = $this->callTop("dingtalk.corp.role.list", [
            'size' => $size,
            'offset' => $offset
        ]);
        return $response;
    }

    /**
     * 批量为员工增加角色信息
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.0oUPEc&treeId=385&articleId=29979&docType=2
     *
     * @param array $rolelidList 角色id list 最大列表长度：20
     * @param array $useridList 员工id list 最大列表长度：100
     * @return bool
     * @throws \think\exception\DbException
     */
    public function corpRoleAddrolesforemps($rolelidList, $useridList) {
        $response = $this->callTop("dingtalk.corp.role.addrolesforemps", [
            'rolelid_list' => join(',', $rolelidList),
            'userid_list' => join(',', $useridList)
        ]);
        return $this->_fetch($response, 'success');
    }

    /**
     * 批量删除员工角的色信息
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.ATtD7m&treeId=385&articleId=29980&docType=2
     *
     * @param array $rolelidList 角色id list 最大列表长度：20
     * @param array $useridList 员工id list 最大列表长度：100
     * @return array|mixed
     * @throws \think\exception\DbException
     */
    public function corpRoleRemoverolesforemps($rolelidList, $useridList) {
        $response = $this->callTop("dingtalk.corp.role.removerolesforemps", [
            'roleid_list' => join(',', $rolelidList),
            'userid_list' => join(',', $useridList)
        ]);
        return $this->_fetch($response, 'success');
    }

    /**
     * 删除角色信息
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.4DDG6J&treeId=385&articleId=29977&docType=2
     *
     * @param string $roleId 角色id
     * @return bool
     * @throws \think\exception\DbException
     */
    public function corpRoleDeleterole($roleId) {
        $response = $this->callTop("dingtalk.corp.role.deleterole", [
            'role_id' => $roleId
        ]);
        return $this->_fetch($response, 'success');
    }

    /**
     * 获取角色组信息
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.chUuBH&treeId=385&articleId=29978&docType=2
     *
     * @param string $groupId 角色组的Id
     * @return array|bool
     * {
     *       "group_name": "职务",
     *       "roles": [
     *          {
     *              "role_id": 301628135,
     *              "role_name": "财务"
     *          }
     *       ]
     * }
     * @throws \think\exception\DbException
     */
    public function corpRoleGetrolegroup($groupId) {
        $response = $this->callTop("dingtalk.corp.role.getrolegroup", [
            'group_id' => $groupId
        ]);
        return $this->_fetch($response, 'role_group');
    }

    /**
     * 查询用户是否开启了钉钉运动
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.zUqueX&treeId=385&articleId=34391&docType=2
     *
     * @param $userid
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function corpHealthStepinfoGetuserstatus($userid) {
        $response = $this->callTop("dingtalk.corp.health.stepinfo.getuserstatus", [
            'userid' => $userid
        ]);
        return $this->_fetch($response, 'status', null);
    }

    /**
     * 批量查询多个用户的钉钉运动步数
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.IB9PFU&treeId=385&articleId=34390&docType=2
     *
     * @param string $userids 员工userid列表，最多传50个
     * @param string $statDate 时间，注意时间格式是YYMMDD
     * @return bool|array
     * [
     *      {
     *          "stat_date" => 20180502,
     *          "step_count" => 9802,
     *          "userid" => "1804684533-64254447"
     *      }
     * ]
     * @throws \think\exception\DbException
     */
    public function corpHealthStepinfoListbyuserid($userids, $statDate) {
        $response = $this->callTop("dingtalk.corp.health.stepinfo.listbyuserid", [
            'userids' => join(',', $userids),
            'stat_date' => $statDate
        ]);
        return $this->_fetch($response, 'stepinfo_list', null);
    }

    /**
     * 添加企业外部联系人
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.A7UDCA&treeId=385&articleId=33400&docType=2
     *
     * @param CorpExtcontactSaveRequest $request
     * @return bool|mixed
     * @throws \ReflectionException
     * @throws \think\exception\DbException
     */
    public function corpExtcontactCreate(CorpExtcontactSaveRequest $request) {
        $response = $this->callTop("dingtalk.corp.extcontact.create", [
            'contact' => json_encode($request->toData())
        ]);
        return $this->_fetch($response, 'userid', null);
    }

    /**
     * 更新企业外部联系人
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.p3GZsX&treeId=385&articleId=33403&docType=2
     *
     * @param CorpExtcontactSaveRequest $request
     * @return bool|mixed
     * @throws \ReflectionException
     * @throws \think\exception\DbException
     */
    public function corpExtcontactUpdate(CorpExtcontactSaveRequest $request) {
        $response = $this->callTop("dingtalk.corp.extcontact.update", [
            'contact' => json_encode($request->toData())
        ]);
        return $this->_fetch($response, 'success', null);
    }

    /**
     * 获取企业外部联系人列表
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.dRG0X6&treeId=385&articleId=33402&docType=2
     *
     * @param int $offset
     * @param int $size
     * @return bool|array
     * [
     *   {
     *          "company_name": "公司名称",
     *          "follower_user_id": "1804684533-64254447",
     *          "label_ids": [
     *              309887389
     *          ],
     *          "mobile": "18896580791",
     *          "name": "姓名",
     *          "share_dept_ids": [],
     *          "share_user_ids": [],
     *          "state_code": "86",
     *          "title": "",
     *          "userid": "1865582850845407"
     *   }
     * ]
     * @throws \think\exception\DbException
     */
    public function corpExtcontactList($offset = 0, $size = 20) {
        $response = $this->callTop("dingtalk.corp.extcontact.list", [
            'offset' => $offset,
            'size' => $size
        ]);
        return $this->_fetch($response, 'results', null);
    }

    /**
     * 获取企业外部联系人详情
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.ZppLVU&treeId=385&articleId=33404&docType=2
     *
     * @param $userId
     * @return bool|array
     *   {
     *          "company_name": "公司名称",
     *          "follower_user_id": "1804684533-64254447",
     *          "label_ids": [
     *              309887389
     *          ],
     *          "mobile": "18896580791",
     *          "name": "姓名",
     *          "share_dept_ids": [],
     *          "share_user_ids": [],
     *          "state_code": "86",
     *          "title": "",
     *          "userid": "1865582850845407"
     *   }
     * @throws \think\exception\DbException
     */
    public function corpExtcontactGet($userId) {
        $response = $this->callTop("dingtalk.corp.extcontact.get", [
            'user_id' => $userId
        ]);
        return $this->_fetch($response, 'result', null);
    }

    /**
     * 获取企业外部联系人标签列表
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.uSmZDl&treeId=385&articleId=33406&docType=2
     *
     * @param int $offset
     * @param int $size
     * @return bool|mixed
     * [
     *     {
     *         "color": -15220075,
     *         "labels": [
     *             {
     *                 "id": 309887389,
     *                 "name": "标签名称"
     *             }
     *         ],
     *         "name": "分组名称"
     *     }
     * ]
     * @throws \think\exception\DbException
     */
    public function corpExtcontactListlabelgroups($offset = 0, $size = 20) {
        $response = $this->callTop("dingtalk.corp.extcontact.listlabelgroups", [
            'offset' => $offset,
            'size' => $size
        ]);
        return $this->_fetch($response, 'results', null);
    }

    /**
     * 按天查询企业考勤排班全量信息
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.BIlYBl&treeId=385&articleId=29082&docType=2
     *
     * @param string $workDate 排班日期:Y-m-d格式
     * @param int $offset
     * @param int $size
     * @return bool|array
     * {
     *     "has_more":false,
     *     "schedules":[
     *         {
     *             "check_type":"OnDuty",                       // 打卡类型，Onduty表示上班打卡，OffDuty表示下班打卡
     *             "group_id":-1,                               // 考勤组id
     *             "plan_check_time":"2018-05-05 11:27:42",     // 打卡时间
     *             "plan_id":18770041280,                       // 排班id
     *             "userid":"1804684533-64254447"               // 用户id
     *         },
     *         {
     *             "check_type":"OnDuty",
     *             "class_id":219775659,                        // 考勤班次id
     *             "class_setting_id":115071448,                // 班次配置id
     *             "approve_id":122112,                         // 审批id
     *             "group_id":218590567,
     *             "plan_check_time":"2018-05-05 09:00:00",
     *             "plan_id":18912474888,
     *             "userid":"26106638845407"
     *         },
     *         {
     *             "check_type":"OffDuty",
     *             "class_id":219775659,
     *             "group_id":218590567,
     *             "plan_check_time":"2018-05-05 18:00:00",
     *             "plan_id":18912474889,
     *             "userid":"26106638845407"
     *         }
     *     ]
     * }
     * @throws \think\exception\DbException
     */
    public function smartworkAttendsListschedule($workDate, $offset = 0, $size = 200) {
        $response = $this->callTop("dingtalk.smartwork.attends.listschedule", [
            'work_date' => $workDate . " 00:00:00",
            'offset' => $offset,
            'size' => $size
        ]);
        return $this->_fetch($response, 'result', null);
    }

    /**
     * 获取公司自身的考勤组列表详情信息
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.3tVLgQ&treeId=355&articleId=29083&docType=2
     *
     * @param int $offset
     * @param int $size
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function smartworkAttendsGetsimplegroups($offset = 0, $size = 10) {
        $response = $this->callTop("dingtalk.smartwork.attends.getsimplegroups", [
            'offset' => $offset,
            'size' => $size
        ]);
        return $this->_fetch($response, 'result', null);
    }

    /**
     * 获取企业内员工的实际打卡记录
     * 比如，企业给一个员工设定的排班是上午9点和下午6点各打一次卡，但是员工在这期间打了多次，该接口会把所有的打卡记录都返回。
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.FCYvrb&treeId=355&articleId=106662&docType=1
     *
     * @param array $userIds 企业内的员工id列表，最多不能超过50个
     * @param string $checkDateFrom 查询考勤打卡记录的起始工作日。格式为：Y-m-d h:i:s
     * @param string $checkDateTo 查询考勤打卡记录的结束工作日。格式为：Y-m-d h:i:s。注意，起始与结束工作日最多相隔7天
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function attendanceListRecord(array $userIds, $checkDateFrom, $checkDateTo) {
        $response = $this->callWithToken("attendance/listRecord", [
            'userIds' => $userIds,
            'checkDateFrom' => $checkDateFrom,
            'checkDateTo' => $checkDateTo
        ], true);

        return $this->_fetch($response, 'recordresult', null);
    }

    /**
     * 获取企业内员工的实际打卡结果
     * 比如，企业给一个员工设定的排班是上午9点和下午6点各打一次卡，即使员工在这期间打了多次，该接口也只会返回两条记录，包括上午的打卡结果和下午的打卡结果
     *
     * @param array $userIdList 员工在企业内的UserID列表
     * @param string $workDateFrom 查询考勤打卡记录的起始工作日，格式为：Y-m-d h:i:s
     * @param string $workDateTo 查询考勤打卡记录的结束工作日，格式为：Y-m-d h:i:s
     * @param int $offset 表示获取考勤数据的起始点，第一次传0，如果还有多余数据，下次获取传的offset值为之前的offset+limit
     * @param int $limit 表示获取考勤数据的条数，最大不能超过50条
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function attendanceList(array $userIdList, $workDateFrom, $workDateTo, $offset = 0, $limit = 50) {
        $response = $this->callWithToken("attendance/list", [
            'userIdList' => $userIdList,
            'workDateFrom' => $workDateFrom,
            'workDateTo' => $workDateTo,
            'offset' => $offset,
            'limit' => $limit
        ], true);

        return $this->_fetch($response, 'recordresult', null);
    }

    /**
     * 计算员工请假时长。
     * 传入请假起止日期，会结合排班表自动计算需要扣除多少工时。
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.n6lbJz&treeId=355&articleId=32125&docType=2
     *
     * @param string $userid 员工在企业内的UserID
     * @param string $fromDate 请假开始时间，格式为：Y-m-d h:i:s
     * @param string $toDate 请假结束时间，格式为：Y-m-d h:i:s
     * @return bool|int 请假时长（单位分钟）
     * @throws \think\exception\DbException
     */
    public function smartworkAttendsGetleaveapproveduration($userid, $fromDate, $toDate) {
        $response = $this->callTop("dingtalk.smartwork.attends.getleaveapproveduration", [
            'userid' => $userid,
            'from_date' => $fromDate,
            'to_date' => $toDate
        ]);
        return $this->_fetch(
            $this->_fetch($response, 'result', null),
            'duration_in_minutes',
            null
        );
    }

    /**
     * 获取用户所在的考勤组
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.Q8e5Fz&treeId=355&articleId=32308&docType=2
     *
     * @param $userid
     * @return bool|array
     * @throws \think\exception\DbException
     */
    public function smartworkAttendsGetusergroup($userid) {
        $response = $this->callTop("dingtalk.smartwork.attends.getusergroup", [
            'userid' => $userid
        ]);
        return $this->_fetch($response, 'result', null);
    }

    /**
     * 按部门获取签到数据
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.76XE64&treeId=355&articleId=106248&docType=1
     *
     * @param string $departmentId 部门id（-1 表示根部门）
     * @param int $startTime 开始时间，精确到毫秒
     * @param int $endTime 结束时间，精确到毫秒。注意开始时间和结束时间的间隔不能大于45 天
     * @param string $order 排序，asc 为正序，desc 为倒序
     * @param int $offset 支持分页查询，与size 参数同时设置时才生效，此参数代表偏移量，从0 开始
     * @param int $size 支持分页查询，与offset 参数同时设置时才生效，此参数代表分页大小，最大100
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function checkinRecord($departmentId, $startTime, $endTime, $order = 'desc', $offset = 0, $size = 100) {
        $response = $this->callWithToken("checkin/record", [
            'department_id' => $departmentId,
            'start_time' => $startTime,
            'end_time' => $endTime,
            'offset' => $offset,
            'size' => $size,
            'order' => $order
        ], false);

        return $this->_fetch($response, 'data', null);
    }

    /**
     * 查询多个用户一段时间范围内的签到记录
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.z9shxR&treeId=355&articleId=29538&docType=2
     *
     * @param array $useridList 需要查询的用户列表，最多10个
     * @param int $startTime 起始时间,单位毫秒
     * @param int $endTime 截止时间，单位毫秒。如果是取1个人的数据，时间范围最大到10天，如果是取多个人的数据，时间范围最大1天。
     * @param int $cursor 分页查询的游标，最开始可以传0
     * @param int $size 分页查询的每页大小，最大100
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function smartworkCheckinRecordGet(array $useridList, $startTime, $endTime, $cursor = 0, $size = 100) {
        $response = $this->callTop("dingtalk.smartwork.checkin.record.get", [
            'userid_list' => join(',', $useridList),
            'start_time' => $startTime,
            'end_time' => $endTime,
            'cursor' => $cursor,
            'size' => $size
        ]);
        return $this->_fetch($response, 'result', null);
    }


    /**
     * 查询企业员工发出的日志列表
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.4quf0J&treeId=355&articleId=31998&docType=2
     *
     * @param int $startTime 查询起始时间
     * @param int $endTime 查询截止时间
     * @param string $userid 用户id
     * @param string $templateName 要查询的模板名称
     * @param int $cursor 查询游标，初始传入0，后续从上一次的返回值中获取
     * @param int $size 每页数据量
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function corpReportList($startTime, $endTime, $userid = null, $templateName = null, $cursor = 0, $size = 10) {
        $response = $this->callTop("dingtalk.corp.report.list", [
            'userid' => $userid,
            'template_name' => $templateName,
            'start_time' => $startTime,
            'end_time' => $endTime,
            'cursor' => $cursor,
            'size' => $size
        ]);
        return $this->_fetch($response, 'result', null);
    }

    /**
     * 列出用户有权限看到的公告列表
     *
     * @param $userid
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function smartworkBlackboardListtopten($userid) {
        $response = $this->callTop("dingtalk.smartwork.blackboard.listtopten", [
            'userid' => $userid
        ]);
        return $this->_fetch($response, 'blackboard_list', null);
    }

    /**
     * 复制审批流
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.C0nzV8&treeId=355&articleId=29325&docType=2
     *
     * @param string $processCode 审批流的唯一码
     * @param string $processName 审批流名称
     * @param string $description 审批流描述
     * @param string $bizCategoryId 业务分类标识（建议采用JAVA包名的命名方式）
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function smartworkBpmsProcessCopy($processCode, $processName = null, $description = null, $bizCategoryId = null) {
        $data = [
            'agent_id' => $this->getAgentId(),
            'process_code' => $processCode
        ];
        if ($processName != null) {
            $data['process_name'] = $processName;
        }
        if ($description != null) {
            $data['description'] = $description;
        }
        if ($bizCategoryId != null) {
            $data['biz_category_id'] = $bizCategoryId;
        }
        $response = $this->callTop("dingtalk.smartwork.bpms.process.copy", $data);
        return $this->_fetch($response, 'result', null);
    }

    /**
     * 更新审批流
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.YF1vZi&treeId=355&articleId=29766&docType=2
     *
     * @param $srcProcessCode
     * @param $targetProcessCode
     * @param null $processName
     * @param null $bizCategoryId
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function smartworkBpmsProcessSync($srcProcessCode, $targetProcessCode, $processName = null, $bizCategoryId = null) {
        $data = [
            'agent_id' => $this->getAgentId(),
            'src_process_code' => $srcProcessCode,
            'target_process_code' => $targetProcessCode
        ];
        if ($processName != null) {
            $data['process_name'] = $processName;
        }
        if ($bizCategoryId != null) {
            $data['biz_category_id'] = $bizCategoryId;
        }
        $response = $this->callTop("dingtalk.smartwork.bpms.process.sync", $data);
        return $this->_fetch($response, 'success', null);
    }

    /**
     * 创建流程审批实例
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.7Fh9PB&treeId=355&articleId=29498&docType=2
     *
     * @param SmartworkBpmsProcessinstanceCreateRequest $request
     * @return bool|mixed
     * @throws \ReflectionException
     * @throws \think\exception\DbException
     */
    public function smartworkBpmsProcessinstanceCreate(SmartworkBpmsProcessinstanceCreateRequest $request) {
        $data = $request->toData(true);
        $data['agent_id'] = $this->getAgentId();
        $response = $this->callTop("dingtalk.smartwork.bpms.processinstance.create", $data);
        return $this->_fetch($response, 'process_instance_id', null);
    }


    /**
     * 批量获取流程审批实例
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.LTRQCe&treeId=355&articleId=29833&docType=2
     *
     * @param string $processCode 流程模板唯一标识
     * @param int $startTime 审批实例开始时间，毫秒级
     * @param int $endTime 审批实例结束时间，毫秒级，默认取当前值
     * @param array $useridList 发起人用户id列表,最大20个
     * @param int $cursor 分页查询的游标，最开始传0，后续传返回参数中的next_cursor值
     * @param int $size 分页参数，每页大小，最多传10
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function smartworkBpmsProcessinstanceList($processCode, $startTime, $endTime = null, $useridList = null, $cursor = 0, $size = 10) {
        if ($endTime == null) {
            $endTime = time() * 1000;
        }
        $data = [
            'process_code' => $processCode,
            'start_time' => $startTime,
            'end_time' => $endTime,
            'cursor' => $cursor,
            'size' => $size
        ];
        if ($useridList != null) {
            $data['userid_list'] = join(",", $useridList);
        }
        $response = $this->callTop("dingtalk.smartwork.bpms.processinstance.list", $data);
        return $this->_fetch($response, 'result', null);
    }

    /**
     * 获取单个审批实例详情
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.mWCrSc&treeId=355&articleId=32977&docType=2
     *
     * @param $processInstanceId
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function smartworkBpmsProcessinstanceGet($processInstanceId) {
        $data = [
            'process_instance_id' => $processInstanceId
        ];
        $response = $this->callTop("dingtalk.smartwork.bpms.processinstance.get", $data);
        return $this->_fetch($response, 'process_instance', null);
    }

    /**
     * 注册事件回调接口
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.F9kK8K&treeId=385&articleId=104975&docType=1#s7
     *
     * @param array $callBackTag 需要监听的事件类型，可选值见CallbackTagEnum
     * @param string $token 加解密需要用到的token，ISV(服务提供商)推荐使用注册套件时填写的token，普通企业可以随机填写
     * @param string $aesKey 数据加密密钥。用于回调数据的加密，长度固定为43个字符，从a-z, A-Z, 0-9共62个字符中选取,您可以随机生成，ISV(服务提供商)推荐使用注册套件时填写的EncodingAESKey
     * @param string $url 接收事件回调的url
     * @return bool
     * @throws \think\exception\DbException
     */
    public function callBackRegisterCallBack(array $callBackTag, $token, $aesKey, $url) {
        $response = $this->callWithToken("call_back/register_call_back", [
            'call_back_tag' => $callBackTag,
            'token' => $token,
            'aes_key' => $aesKey,
            'url' => $url
        ], true);

        return $this->_fetch($response, "errcode") === 0;
    }

    /**
     * 查询事件回调接口
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.F9kK8K&treeId=385&articleId=104975&docType=1#s8
     *
     * @return array
     * @throws \think\exception\DbException
     */
    public function callBackGetCallBack() {
        $response = $this->callWithToken("call_back/get_call_back", [], false);
        return $response;
    }

    /**
     * 更新事件回调接口
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.F9kK8K&treeId=385&articleId=104975&docType=1#s9
     *
     * @param array $callBackTag 需要监听的事件类型，可选值见CallbackTagEnum
     * @param string $token 加解密需要用到的token，ISV(服务提供商)推荐使用注册套件时填写的token，普通企业可以随机填写
     * @param string $aesKey 数据加密密钥。用于回调数据的加密，长度固定为43个字符，从a-z, A-Z, 0-9共62个字符中选取,您可以随机生成，ISV(服务提供商)推荐使用注册套件时填写的EncodingAESKey
     * @param string $url 接收事件回调的url
     * @return bool
     * @throws \think\exception\DbException
     */
    public function callBackUpdateCallBack(array $callBackTag, $token, $aesKey, $url) {
        $response = $this->callWithToken("call_back/update_call_back", [
            'call_back_tag' => $callBackTag,
            'token' => $token,
            'aes_key' => $aesKey,
            'url' => $url
        ], true);

        return $this->_fetch($response, "errcode") === 0;
    }

    /**
     * 删除事件回调接口
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.F9kK8K&treeId=385&articleId=104975&docType=1#s10
     *
     * @return bool
     * @throws \think\exception\DbException
     */
    public function callBackDeleteCallBack() {
        $response = $this->callWithToken("call_back/delete_call_back", [], false);
        return $this->_fetch($response, "errcode") === 0;
    }

    /**
     * 获取回调失败的结果
     * https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.F9kK8K&treeId=385&articleId=104975&docType=1#s11
     *
     * @return array
     * @throws \think\exception\DbException
     */
    public function callBackGetCallBackFailedResult() {
        $response = $this->callWithToken("call_back/get_call_back_failed_result", [], false);
        return $response;
    }

    /**
     * 发起待办事项
     * 企业可以调用该接口发起一个待办事项，该待办事项会出现在钉钉客户端“待办事项”页面，与钉钉审批待办事项并列。
     * 目前待办事项有防骚扰控制，具体为：
     * 1、每人每天最多收到一条表单内容相同的待办。触发这个限制，会返回错误码854001
     * 2、每人每天最多收到100条待办。触发这个限制，会返回错误码854002
     *
     * https://open-doc.dingtalk.com/microapp/serverapi2/gdzay4
     *
     * @param WorkrecordAddRequest $request
     * @throws \ReflectionException
     * @throws \think\exception\DbException
     * @return string
     */
    public function workrecordAdd(WorkrecordAddRequest $request) {
        $response = $this->callWithToken("workrecord/add", $request->toData(), true);
        return $this->_fetch($response, "record_id");
    }

    /**
     * 更新待办事项状态
     * 调用成功后，该待办事项会该用户的“待办事项”列表页面消失。
     * https://open-doc.dingtalk.com/microapp/serverapi2/sltmwf
     *
     * @param $userid
     * @param $recordId
     * @return bool
     * @throws \think\exception\DbException
     */
    public function workrecordUpdate($userid, $recordId) {
        $response = $this->callWithToken("workrecord/update", [
            'userid' => $userid,
            'record_id' => $recordId
        ], true);

        return $this->_fetch($response, 'result', false);
    }

    /**
     * 分页获取用户的待办事项列表
     *
     * @param $userid
     * @param int $status 待办事项状态，0表示未完成，1表示完成
     * @param int $offset
     * @param int $limit
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function workrecordGetbyuserid($userid, $status, $offset = 0, $limit = 50) {
        $response = $this->callWithToken("workrecord/getbyuserid", [
            'userid' => $userid,
            'status' => $status,
            'offset' => $offset,
            'limit' => $limit
        ], true);

        return $this->_fetch($response, 'records');
    }

    /**
     * 获取员工花名册字段信息(确定企业已开通智能人事模块)
     * https://open-doc.dingtalk.com/microapp/serverapi2/gnct3y
     * 如果传入的userid是已离职的，也会将用户信息返回
     *
     * @param array $useridList 员工userid列表，最大列表长度：20
     * @param null|array $fieldFilterList 需要获取的花名册字段列表，最大列表长度：20
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function smartworkHrmEmployeeList($useridList, $fieldFilterList = null) {
        $data = [];
        if (is_array($useridList)) {
            $useridList = join(",", $useridList);
        }
        if (is_array($fieldFilterList)) {
            $fieldFilterList = join(",", $fieldFilterList);
        }
        $data['userid_list'] = $useridList;
        if ($fieldFilterList) {
            $data['field_filter_list'] = $fieldFilterList;
        }
        $response = $this->callWithToken("smartwork/hrm/employee/list", $data, true);

        return $this->_fetch($response, 'result');
    }

    /**
     * 查询企业离职员工列表
     * https://open-doc.dingtalk.com/microapp/serverapi2/ndkn2l
     *
     * @param int $offset
     * @param int $size 最大50
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function smartworkHrmEmployeeQuerydimission($offset = 0, $size = 50) {
        $response = $this->callWithToken("smartwork/hrm/employee/querydimission", [
            'offset' => $offset,
            'size' => $size
        ], true);

        return $this->_fetch($response, 'result');
    }

    /**
     * 查询企业待入职员工列表
     * https://open-doc.dingtalk.com/microapp/serverapi2/izp962
     *
     * @param int $offset
     * @param int $size 最大50
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function smartworkHrmEmployeeQuerypreentry($offset = 0, $size = 50) {
        $response = $this->callWithToken("smartwork/hrm/employee/querypreentry", [
            'offset' => $offset,
            'size' => $size
        ], true);

        return $this->_fetch($response, 'result');
    }

    /**
     * 查询企业在职员工列表
     * https://open-doc.dingtalk.com/microapp/serverapi2/at3ety
     *
     * @param null|array $statusList
     * @param int $offset
     * @param int $size
     * @return bool|mixed
     * @throws \think\exception\DbException
     * @throws \ReflectionException
     */
    public function smartworkHrmEmployeeQueryonjob($statusList = null, $offset = 0, $size = 20) {
        if ($statusList == null) {
            $statusList = EmployeeUserStatusEnum::all();
        }
        if (!is_array($statusList)) {
            $statusList = [$statusList];
        }
        $response = $this->callWithToken("smartwork/hrm/employee/queryonjob", [
            'status_list' => join(",", $statusList),
            'offset' => $offset,
            'size' => $size
        ], true);

        return $this->_fetch($response, 'result');
    }

    /**
     * 获取离职员工离职信息(根据员工userid列表，批量查询员工的离职信息。传入非离职员工userid，不会返回信息。)
     * https://open-doc.dingtalk.com/microapp/serverapi2/bifilc
     *
     * @param array $useridList 员工userid列表，最大长度50
     * @return bool|mixed
     * @throws \think\exception\DbException
     */
    public function smartworkHrmEmployeeListdimission($useridList) {
        if (!is_array($useridList)) {
            $useridList = [$useridList];
        }
        $response = $this->callWithToken("smartwork/hrm/employee/listdimission", [
            'userid_list' => join(",", $useridList)
        ], true);

        return $this->_fetch($response, 'result');
    }
}
